diff --git a/.gitignore b/.gitignore index 2b4add534..4169827c4 100755 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ *iml *.iml */build -/mainframer.sh +/mainframer +/.mainframer *.apk \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..6e053f2d1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +- Many performance improvements +- Stability improvements and bug fixes +- Upstream merge +- Fix PervEden search +- Add ability to use high-quality thumbnails on nhentai +- Enable PervEden link importing \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 160096473..964c7100f 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,10 +35,6 @@ android { buildToolsVersion "26.0.2" publishNonDefault true - dexOptions { - javaMaxHeapSize "4g" - } - defaultConfig { applicationId "eu.kanade.tachiyomi.eh2" minSdkVersion 16 @@ -96,6 +92,9 @@ android { exclude 'META-INF/LICENSE' exclude 'META-INF/LICENSE.txt' exclude 'META-INF/NOTICE' + + // Compatibility for two RxJava versions (EXH) + exclude 'META-INF/rxjava.properties' } lintOptions { @@ -237,17 +236,17 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" //Pin lock view (EXH) - compile 'com.andrognito.pinlockview:pinlockview:2.1.0' + implementation 'com.andrognito.pinlockview:pinlockview:2.1.0' //Reprint (EXH) - compile 'com.github.ajalt.reprint:core:3.2.0@aar' // required: supports marshmallow devices - compile 'com.github.ajalt.reprint:rxjava:3.2.0@aar' // optional: the RxJava 1 interface + implementation 'com.github.ajalt.reprint:core:3.2.0@aar' // required: supports marshmallow devices + implementation 'com.github.ajalt.reprint:rxjava:3.2.0@aar' // optional: the RxJava 1 interface //Swirl (EXH) - compile 'com.mattprecious.swirl:swirl:1.0.0' + implementation 'com.mattprecious.swirl:swirl:1.0.0' //RxJava 2 interop for Realm (EXH) - compile 'com.lvla.android:rxjava2-interop-kt:0.2.1' + implementation 'com.lvla.android:rxjava2-interop-kt:0.2.1' } buildscript { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fffcac965..b4a662fa2 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -143,6 +143,10 @@ android:host="nhentai.net" android:pathPrefix="/g/" android:scheme="https"/> + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 5e677eacf..a936e8ee7 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -117,4 +117,5 @@ object PreferenceKeys { fun trackToken(syncId: Int) = "track_token_$syncId" + const val eh_nh_useHighQualityThumbs = "eh_nh_hq_thumbs" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 772d16d15..1fa2d2c03 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -201,5 +201,7 @@ class PreferencesHelper(val context: Context) { fun lockLength() = rxPrefs.getInteger("lock_length", -1) fun lockUseFingerprint() = rxPrefs.getBoolean("lock_finger", false) + + fun eh_useHighQualityThumbs() = rxPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false) // <-- EH } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 30813d069..de5a69fbe 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.source.online.russian.Mintmanga import eu.kanade.tachiyomi.source.online.russian.Readmanga import eu.kanade.tachiyomi.util.hasPermission import exh.* +import exh.metadata.models.PervEdenLang import org.yaml.snakeyaml.Yaml import rx.Observable import timber.log.Timber @@ -93,9 +94,10 @@ open class SourceManager(private val context: Context) { if(prefs.enableExhentai().getOrDefault()) { exSrcs += EHentai(EXH_SOURCE_ID, true, context) } - exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, "en") - exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, "it") + exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en) + exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it) exSrcs += NHentai(context) + exSrcs += HentaiCafe() return exSrcs } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/LewdSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/LewdSource.kt new file mode 100644 index 000000000..12d8e5041 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/LewdSource.kt @@ -0,0 +1,55 @@ +package eu.kanade.tachiyomi.source.online + +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.model.SManga +import exh.metadata.models.GalleryQuery +import exh.metadata.models.PervEdenGalleryMetadata +import exh.metadata.models.SearchableGalleryMetadata +import exh.util.createUUIDObj +import exh.util.defRealm +import exh.util.realmTrans +import rx.Observable + +/** + * LEWD! + */ +interface LewdSource : CatalogueSource { + fun queryAll(): GalleryQuery + + fun queryFromUrl(url: String): GalleryQuery + + val metaParser: M.(I) -> Unit + + fun parseToManga(query: GalleryQuery, input: I): SManga + = realmTrans { realm -> + val meta = realm.copyFromRealm(query.query(realm).findFirst() + ?: realm.createUUIDObj(queryAll().clazz.java)) + + metaParser(meta, input) + + realm.copyToRealmOrUpdate(meta) + + SManga.create().apply { + meta.copyTo(this) + } + } + + fun lazyLoadMeta(query: GalleryQuery, parserInput: Observable): Observable { + return defRealm { realm -> + val possibleOutput = query.query(realm).findFirst() + + if(possibleOutput == null) + parserInput.map { + realmTrans { realm -> + val meta = realm.createUUIDObj(queryAll().clazz.java) + + metaParser(meta, it) + + realm.copyFromRealm(meta) + } + } + else + Observable.just(realm.copyFromRealm(possibleOutput)) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt index edec28b86..a3015d141 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt @@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.util.asJsoup import exh.metadata.* import exh.metadata.models.ExGalleryMetadata @@ -24,13 +25,11 @@ import okhttp3.CacheControl import okhttp3.Headers import okhttp3.Request import org.jsoup.nodes.Document -import exh.GalleryAdder import exh.util.* -import io.realm.Realm class EHentai(override val id: Long, val exh: Boolean, - val context: Context) : HttpSource() { + val context: Context) : HttpSource(), LewdSource { val schema: String get() = if(prefs.secureEXH().getOrDefault()) @@ -49,8 +48,6 @@ class EHentai(override val id: Long, val prefs: PreferencesHelper by injectLazy() - val galleryAdder = GalleryAdder() - /** * Gallery list entry */ @@ -185,90 +182,80 @@ class EHentai(override val id: Long, /** * Parse gallery page to metadata model */ - 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) + override fun mangaDetailsParse(response: Response): SManga { + return parseToManga(queryFromUrl(response.request().url().toString()), response) + } - val metdata = (realm.loadEh(gId, gToken, exh) - ?: realm.createUUIDObj(ExGalleryMetadata::class.java)) - with(metdata) { - this.url = url - this.gId = gId - this.gToken = gToken + override val metaParser: ExGalleryMetadata.(Response) -> Unit = { response -> + with(response.asJsoup()) { + url = response.request().url().encodedPath()!! + gId = ExGalleryMetadata.galleryId(url!!) + gToken = ExGalleryMetadata.galleryToken(url!!) - exh = this@EHentai.exh - title = select("#gn").text().nullIfBlank()?.trim() + exh = this@EHentai.exh + title = select("#gn").text().nullIfBlank()?.trim() - altTitle = select("#gj").text().nullIfBlank()?.trim() + altTitle = select("#gj").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('/') + thumbnailUrl = select("#gd1 div").attr("style").nullIfBlank()?.let { + it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')')) + } + genre = select(".ic").parents().attr("href").nullIfBlank()?.trim()?.substringAfterLast('/') - uploader = select("#gdn").text().nullIfBlank()?.trim() + 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() + //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() } } - } - } + } + } + } - //Parse ratings - ignore { - averageRating = select("#rating_label") - .text() - .removePrefix("Average:") - .trim() - .nullIfBlank() - ?.toDouble() - ratingCount = select("#rating_count") - .text() - .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(":") - tags.addAll(it.select("div").map { - Tag(namespace, it.text().trim(), it.hasClass("gtl")) - }) - } - - //Copy metadata to manga - SManga.create().apply { - copyTo(this) - } + //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")) + }) } } } @@ -323,7 +310,7 @@ class EHentai(override val id: Long, if (favNames == null) favNames = doc.getElementsByClass("nosel").first().children().filter { it.children().size >= 3 - }.map { it.child(2).text() }.filterNotNull() + }.mapNotNull { it.child(2).text() } //Next page page++ @@ -384,9 +371,9 @@ class EHentai(override val id: Long, } fun buildCookies(cookies: Map) - = cookies.entries.map { + = cookies.entries.joinToString(separator = "; ", postfix = ";") { "${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}" - }.joinToString(separator = "; ", postfix = ";") + } fun addParam(url: String, param: String, value: String) = Uri.parse(url) @@ -465,6 +452,9 @@ class EHentai(override val id: Long, else "E-Hentai" + override fun queryAll() = ExGalleryMetadata.EmptyQuery() + override fun queryFromUrl(url: String) = ExGalleryMetadata.UrlQuery(url, exh) + companion object { val QUERY_PREFIX = "?f_apply=Apply+Filter" val TR_SUFFIX = "TR" diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt index f5c1967cb..19385ebd5 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/NHentai.kt @@ -2,10 +2,7 @@ package eu.kanade.tachiyomi.source.online.all import android.content.Context import android.net.Uri -import com.github.salomonbrys.kotson.get -import com.github.salomonbrys.kotson.int -import com.github.salomonbrys.kotson.long -import com.github.salomonbrys.kotson.string +import com.github.salomonbrys.kotson.* import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject @@ -16,17 +13,12 @@ import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.LewdSource import exh.NHENTAI_SOURCE_ID -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 exh.util.* import okhttp3.Request import okhttp3.Response import rx.Observable @@ -36,7 +28,7 @@ import timber.log.Timber * NHentai source */ -class NHentai(context: Context) : HttpSource() { +class NHentai(context: Context) : HttpSource(), LewdSource { override fun fetchPopularManga(page: Int): Observable { //TODO There is currently no way to get the most popular mangas //TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen @@ -78,8 +70,10 @@ class NHentai(context: Context) : HttpSource() { override fun latestUpdatesParse(response: Response) = parseResultPage(response) - override fun mangaDetailsParse(response: Response) - = parseGallery(jsonParser.parse(response.body()!!.string()).asJsonObject) + override fun mangaDetailsParse(response: Response): SManga { + val obj = jsonParser.parse(response.body()!!.string()).asJsonObject + return parseToManga(NHentaiMetadata.Query(obj["id"].long), obj) + } //Used so we can use a different URL for fetching manga details and opening the details in the browser override fun fetchMangaDetails(manga: SManga): Observable { @@ -102,7 +96,8 @@ class NHentai(context: Context) : HttpSource() { val error = res.get("error") if(error == null) { val results = res.getAsJsonArray("result")?.map { - parseGallery(it.asJsonObject) + val obj = it.asJsonObject + parseToManga(NHentaiMetadata.Query(obj["id"].long), obj) } val numPages = res.get("num_pages")?.int if(results != null && numPages != null) @@ -113,70 +108,65 @@ class NHentai(context: Context) : HttpSource() { return MangasPage(emptyList(), false) } - fun rawParseGallery(obj: JsonObject) = realmTrans { realm -> - val nhId = obj.get("id").asLong + override val metaParser: NHentaiMetadata.(JsonObject) -> Unit = { obj -> + nhId = obj["id"].asLong - realm.copyFromRealm((realm.loadNhentai(nhId) - ?: realm.createUUIDObj(NHentaiMetadata::class.java)).apply { - this.nhId = nhId + uploadDate = obj["upload_date"].nullLong - uploadDate = obj.get("upload_date")?.notNull()?.long + favoritesCount = obj["num_favorites"].nullLong - favoritesCount = obj.get("num_favorites")?.notNull()?.long + mediaId = obj["media_id"].nullString - mediaId = obj.get("media_id")?.notNull()?.string + obj["title"].nullObj?.let { it -> + japaneseTitle = it["japanese"].nullString + shortTitle = it["pretty"].nullString + englishTitle = it["english"].nullString + } - obj.get("title")?.asJsonObject?.let { - japaneseTitle = it.get("japanese")?.notNull()?.string - shortTitle = it.get("pretty")?.notNull()?.string - englishTitle = it.get("english")?.notNull()?.string + obj["images"].nullObj?.let { + coverImageType = it["cover"]?.get("t").nullString + it["pages"].nullArray?.mapNotNull { + it?.asJsonObject?.get("t").nullString + }?.map { + PageImageType(it) + }?.let { + pageImageTypes.clear() + pageImageTypes.addAll(it) } + thumbnailImageType = it["thumbnail"]?.get("t").nullString + } - 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 - } + scanlator = obj["scanlator"].nullString - 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.add(Tag(it.first!!, it.second!!, false)) - } - }) - } - - fun parseGallery(obj: JsonObject) = rawParseGallery(obj).let { - SManga.create().apply { - it.copyTo(this) + obj["tags"]?.asJsonArray?.map { + val asObj = it.asJsonObject + Pair(asObj["type"].nullString, asObj["name"].nullString) + }?.apply { + tags.clear() + }?.forEach { + if(it.first != null && it.second != null) + tags.add(Tag(it.first!!, it.second!!, false)) } } fun lazyLoadMetadata(url: String) = defRealm { realm -> - val meta = realm.loadNhentai(NHentaiMetadata.nhIdFromUrl(url)) - if(meta == null) + val meta = NHentaiMetadata.UrlQuery(url).query(realm).findFirst() + if(meta == null) { client.newCall(urlToDetailsRequest(url)) .asObservableSuccess() .map { - rawParseGallery(jsonParser.parse(it.body()!!.string()) - .asJsonObject) - }.first() - else + realmTrans { realm -> + realm.copyFromRealm(realm.createUUIDObj(queryAll().clazz.java).apply { + metaParser(this, + jsonParser.parse(it.body()!!.string()).asJsonObject) + }) + } + } + .first() + } else { Observable.just(realm.copyFromRealm(meta)) + } } override fun fetchChapterList(manga: SManga) @@ -184,8 +174,7 @@ class NHentai(context: Context) : HttpSource() { listOf(SChapter.create().apply { url = manga.url name = "Chapter" - //TODO Get this working later -// date_upload = it.uploadDate ?: 0 + date_upload = ((it.uploadDate ?: 0) * 1000) chapter_number = 1f }) }!! @@ -241,6 +230,9 @@ class NHentai(context: Context) : HttpSource() { override val supportsLatest = true + override fun queryAll() = NHentaiMetadata.EmptyQuery() + override fun queryFromUrl(url: String) = NHentaiMetadata.UrlQuery(url) + companion object { val jsonParser by lazy { JsonParser() diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/PervEden.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/PervEden.kt index a867a40e7..85f758961 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/PervEden.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/PervEden.kt @@ -3,32 +3,29 @@ package eu.kanade.tachiyomi.source.online.all import android.net.Uri import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.util.ChapterRecognition import eu.kanade.tachiyomi.util.asJsoup -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.metadata.models.* import exh.util.UriFilter import exh.util.UriGroup -import exh.util.createUUIDObj -import exh.util.realmTrans +import exh.util.urlImportFetchSearchManga import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element import org.jsoup.nodes.TextNode -import timber.log.Timber import java.text.SimpleDateFormat import java.util.* -class PervEden(override val id: Long, override val lang: String) : ParsedHttpSource() { +class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSource(), + LewdSource { override val supportsLatest = true override val name = "Perv Eden" override val baseUrl = "http://www.perveden.com" + override val lang = pvLang.name override fun popularMangaSelector() = "#topManga > ul > li" @@ -45,6 +42,12 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou override fun popularMangaNextPageSelector(): String? = null + //Support direct URL importing + override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = + urlImportFetchSearchManga(query, { + super.fetchSearchManga(page, query, filters) + }) + override fun searchMangaSelector() = "#mangaList > tbody > tr" override fun searchMangaFromElement(element: Element): SManga { @@ -89,6 +92,7 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val uri = Uri.parse("$baseUrl/$lang/$lang-directory/").buildUpon() uri.appendQueryParameter("page", page.toString()) + uri.appendQueryParameter("title", query) filters.forEach { if(it is UriFilter) it.addToUri(uri) } @@ -99,77 +103,74 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou throw NotImplementedError("Unused method called!") } - override fun mangaDetailsParse(document: Document): SManga { - realmTrans { realm -> - val url = document.location() - val metadata = (realm.loadPervEden(PervEdenGalleryMetadata.pvIdFromUrl(url), id) - ?: realm.createUUIDObj(PervEdenGalleryMetadata::class.java)) - with(metadata) { - this.url = url + override val metaParser: PervEdenGalleryMetadata.(Document) -> Unit = { document -> + url = Uri.parse(document.location()).path - lang = this@PervEden.lang + pvId = PervEdenGalleryMetadata.pvIdFromUrl(url!!) - title = document.getElementsByClass("manga-title").first()?.text() + lang = this@PervEden.lang - thumbnailUrl = "http:" + document.getElementsByClass("mangaImage2").first()?.child(0)?.attr("src") + title = document.getElementsByClass("manga-title").first()?.text() - val rightBoxElement = document.select(".rightBox:not(.info)").first() + thumbnailUrl = "http:" + document.getElementsByClass("mangaImage2").first()?.child(0)?.attr("src") - 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.add(Tag("artist", it.text().toLowerCase(), false)) - } - } - "Genres" -> { - if(it is Element && it.tagName() == "a") - tags.add(Tag("genre", it.text().toLowerCase(), false)) - } - "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 - } - } + val rightBoxElement = document.select(".rightBox:not(.info)").first() + + altTitles.clear() + 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.add(Tag("artist", it.text().toLowerCase(), false)) + } + } + "Genres" -> { + if(it is Element && it.tagName() == "a") + tags.add(Tag("genre", it.text().toLowerCase(), false)) + } + "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() - - return SManga.create().apply { - copyTo(this) - } } } + + rating = document.getElementById("rating-score")?.attr("value")?.toFloat() } + override fun mangaDetailsParse(document: Document): SManga + = parseToManga(queryFromUrl(document.location()), document) + override fun latestUpdatesRequest(page: Int): Request { - val num = if(lang == "en") "0" - else if(lang == "it") "1" - else throw NotImplementedError("Unimplemented language!") + val num = when (lang) { + "en" -> "0" + "it" -> "1" + else -> throw NotImplementedError("Unimplemented language!") + } return GET("$baseUrl/ajax/news/$page/$num/0/") } @@ -201,6 +202,9 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou override fun imageUrlParse(document: Document) = "http:" + document.getElementById("mainImg").attr("src")!! + override fun queryAll() = PervEdenGalleryMetadata.EmptyQuery() + override fun queryFromUrl(url: String) = PervEdenGalleryMetadata.UrlQuery(url, PervEdenLang.source(id)) + override fun getFilterList() = FilterList ( AuthorFilter(), ArtistFilter(), @@ -223,7 +227,7 @@ class PervEden(override val id: Long, override val lang: String) : ParsedHttpSou } //Explicit type arg for listOf() to workaround this: KT-16570 - class ReleaseYearGroup : UriGroup>("Release Year", listOf>( + class ReleaseYearGroup : UriGroup>("Release Year", listOf( ReleaseYearRangeFilter(), ReleaseYearYearFilter() )) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HentaiCafe.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HentaiCafe.kt new file mode 100644 index 000000000..e8f998d94 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/HentaiCafe.kt @@ -0,0 +1,194 @@ +package eu.kanade.tachiyomi.source.online.english + +import android.net.Uri +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.LewdSource +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import exh.HENTAI_CAFE_SOURCE_ID +import exh.metadata.models.HentaiCafeMetadata +import exh.metadata.models.HentaiCafeMetadata.Companion.BASE_URL +import exh.metadata.models.Tag +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable + +class HentaiCafe : ParsedHttpSource(), LewdSource { + override val id = HENTAI_CAFE_SOURCE_ID + + override val lang = "en" + + override val supportsLatest = true + + override fun queryAll() = HentaiCafeMetadata.EmptyQuery() + override fun queryFromUrl(url: String) = HentaiCafeMetadata.UrlQuery(url) + + override val name = "Hentai Cafe" + override val baseUrl = "https://hentai.cafe" + + override fun popularMangaSelector() = throw UnsupportedOperationException("Unused method called!") + override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!") + override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Unused method called!") + override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException("Unused method called!") + override fun fetchPopularManga(page: Int) = fetchLatestUpdates(page) + + override fun searchMangaSelector() = "article.post" + override fun searchMangaFromElement(element: Element): SManga { + val thumb = element.select(".entry-thumb > img") + val title = element.select(".entry-title > a") + + return SManga.create().apply { + setUrlWithoutDomain(title.attr("href")) + this.title = title.text() + + thumbnail_url = thumb.attr("src") + } + } + override fun searchMangaNextPageSelector() = ".x-pagination > ul > li:last-child > a.prev-next" + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = if(query.isNotBlank()) { + //Filter by query + "$baseUrl/page/$page/?s=${Uri.encode(query)}" + } else if(filters.filterIsInstance().any { it.state }) { + //Filter by book + "$baseUrl/category/book/page/$page/" + } else { + //Filter by tag + val tagFilter = filters.filterIsInstance().first() + + if(tagFilter.state == 0) throw IllegalArgumentException("No filters active, no query active! What to filter?") + + val tag = tagFilter.values[tagFilter.state] + "$baseUrl/tag/${tag.id}/page/$page/" + } + + return GET(url) + } + + override fun latestUpdatesSelector() = searchMangaSelector() + override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element) + override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector() + override fun latestUpdatesRequest(page: Int) = GET("$BASE_URL/page/$page/") + + override fun mangaDetailsParse(document: Document): SManga { + return parseToManga(queryFromUrl(document.location()), document) + } + + override fun chapterListSelector() = throw UnsupportedOperationException("Unused method called!") + override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!") + + override fun fetchChapterList(manga: SManga): Observable> { + return lazyLoadMeta(queryFromUrl(manga.url), + client.newCall(mangaDetailsRequest(manga)).asObservableSuccess().map { it.asJsoup() } + ).map { + listOf(SChapter.create().apply { + url = "/manga/read/${it.readerId}/en/0/1/" + + name = "Chapter" + + chapter_number = 1f + }) + } + } + + override fun pageListParse(document: Document): List { + val pageItems = document.select(".dropdown > li > a") + + return pageItems.mapIndexed { index, element -> + Page(index, element.attr("href")) + } + } + + override fun imageUrlParse(document: Document) + = document.select("#page img").attr("src") + + override val metaParser: HentaiCafeMetadata.(Document) -> Unit = { + val content = it.getElementsByClass("content") + val eTitle = content.select("h3") + + url = Uri.decode(it.location()) + title = eTitle.text() + + tags.clear() + val eDetails = content.select("p > a[rel=tag]") + eDetails.forEach { + val href = it.attr("href") + val parsed = Uri.parse(href) + val firstPath = parsed.pathSegments.first() + + when(firstPath) { + "tag" -> tags.add(Tag("tag", it.text(), false)) + "artist" -> { + artist = it.text() + tags.add(Tag("artist", it.text(), false)) + } + } + } + + readerId = Uri.parse(content.select("a[title=Read]").attr("href")).pathSegments[2] + } + + override fun getFilterList() = FilterList( + TagFilter(), + ShowBooksOnlyFilter() + ) + + class ShowBooksOnlyFilter : Filter.CheckBox("Show books only") + + class TagFilter : Filter.Select("Filter by tag", listOf( + "???" to "None", + + "ahegao" to "Ahegao", + "anal" to "Anal", + "big-ass" to "Big ass", + "big-breast" to "Big Breast", + "bondage" to "Bondage", + "cheating" to "Cheating", + "chubby" to "Chubby", + "condom" to "Condom", + "cosplay" to "Cosplay", + "cunnilingus" to "Cunnilingus", + "dark-skin" to "Dark skin", + "defloration" to "Defloration", + "exhibitionism" to "Exhibitionism", + "fellatio" to "Fellatio", + "femdom" to "Femdom", + "flat-chest" to "Flat chest", + "full-color" to "Full color", + "glasses" to "Glasses", + "group" to "Group", + "hairy" to "Hairy", + "handjob" to "Handjob", + "harem" to "Harem", + "housewife" to "Housewife", + "incest" to "Incest", + "large-breast" to "Large Breast", + "lingerie" to "Lingerie", + "loli" to "Loli", + "masturbation" to "Masturbation", + "nakadashi" to "Nakadashi", + "netorare" to "Netorare", + "office-lady" to "Office Lady", + "osananajimi" to "Osananajimi", + "paizuri" to "Paizuri", + "pettanko" to "Pettanko", + "rape" to "Rape", + "schoolgirl" to "Schoolgirl", + "sex-toys" to "Sex Toys", + "shota" to "Shota", + "stocking" to "Stocking", + "swimsuit" to "Swimsuit", + "teacher" to "Teacher", + "tsundere" to "Tsundere", + "uncensored" to "uncensored", + "x-ray" to "X-ray" + ).map { HCTag(it.first, it.second) }.toTypedArray() + ) + + class HCTag(val id: String, val displayName: String) { + override fun toString() = displayName + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index f0a6aba2c..b0feddf7c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -4,9 +4,6 @@ import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.database.models.Manga import exh.* import exh.metadata.metadataClass -import exh.metadata.models.ExGalleryMetadata -import exh.metadata.models.NHentaiMetadata -import exh.metadata.models.PervEdenGalleryMetadata import exh.metadata.models.SearchableGalleryMetadata import exh.metadata.syncMangaIds import exh.search.SearchEngine @@ -89,7 +86,7 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) : val meta: RealmResults = if (it.value.isNotEmpty()) searchEngine.filterResults(it.value.where(), parsedQuery, - it.value.first().titleFields) + it.value.first()!!.titleFields) .findAllSorted(SearchableGalleryMetadata::mangaId.name).apply { totalFilteredSize += size } @@ -132,7 +129,7 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) : } } } catch (e: Exception) { - Timber.w(e, "Could not filter manga!", manga.manga) + Timber.w(e, "Could not filter manga! %s", manga.manga) } //Fallback to regular filter diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 07f69edde..a22a713a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -4,6 +4,7 @@ import android.app.Dialog import android.os.Bundle import android.support.v7.preference.PreferenceScreen import android.view.View +import android.widget.Toast import com.afollestad.materialdialogs.MaterialDialog import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.FadeChangeHandler @@ -16,6 +17,9 @@ import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.util.toast +import exh.ui.migration.MetadataFetchDialog +import exh.util.realmTrans +import io.realm.Realm import rx.Observable import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers @@ -69,6 +73,38 @@ class SettingsAdvancedController : SettingsController() { onClick { LibraryUpdateService.start(context, target = Target.TRACKING) } } + preferenceCategory { + title = "Gallery metadata" + isPersistent = false + + preference { + title = "Migrate library metadata" + isPersistent = false + key = "ex_migrate_library" + summary = "Fetch the library metadata to enable tag searching in the library. This button will be visible even if you have already fetched the metadata" + + onClick { + activity?.let { + MetadataFetchDialog().askMigration(it, true) + } + } + } + + preference { + title = "Clear library metadata" + isPersistent = false + key = "ex_clear_metadata" + summary = "Clear all library metadata. Disables tag searching in the library" + + onClick { + realmTrans { + it.deleteAll() + } + + context.toast("Library metadata cleared!") + } + } + } } private fun clearChapterCache() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt index 2986b6525..d39f9fee8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhController.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.setting import android.support.v7.preference.PreferenceScreen import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.FadeChangeHandler -import exh.ui.migration.MetadataFetchDialog import exh.ui.login.LoginController import rx.android.schedulers.AndroidSchedulers @@ -124,23 +123,5 @@ class SettingsEhController : SettingsController() { "tr_20" ) }.dependency = "enable_exhentai" - - preferenceCategory { - title = "Advanced" - isPersistent = false - - preference { - title = "Migrate library metadata" - isPersistent = false - key = "ex_migrate_library" - summary = "Fetch the library metadata to enable tag searching in the library. This button will be visible even if you have already fetched the metadata" - - onClick { - activity?.let { - MetadataFetchDialog().askMigration(it, true) - } - } - } - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index 82fa7e504..b98714864 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -48,6 +48,12 @@ class SettingsMainController : SettingsController() { titleRes = R.string.pref_category_eh onClick { navigateTo(SettingsEhController()) } } + preference { + iconRes = R.drawable.eh_ic_nhlogo_color + iconTint = tintColor + titleRes = R.string.pref_category_nh + onClick { navigateTo(SettingsNhController()) } + } preference { iconRes = R.drawable.ic_code_black_24dp iconTint = tintColor diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsNhController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsNhController.kt new file mode 100755 index 000000000..62e184db6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsNhController.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.support.v7.preference.PreferenceScreen +import eu.kanade.tachiyomi.data.preference.PreferenceKeys + +/** + * EH Settings fragment + */ + +class SettingsNhController : SettingsController() { + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + title = "nhentai" + + switchPreference { + title = "Use high-quality thumbnails" + summary = "May slow down search results" + key = PreferenceKeys.eh_nh_useHighQualityThumbs + defaultValue = false + } + } +} diff --git a/app/src/main/java/exh/EHSourceHelpers.kt b/app/src/main/java/exh/EHSourceHelpers.kt index bcccc1ded..14b62a601 100755 --- a/app/src/main/java/exh/EHSourceHelpers.kt +++ b/app/src/main/java/exh/EHSourceHelpers.kt @@ -15,6 +15,8 @@ val PERV_EDEN_IT_SOURCE_ID = LEWD_SOURCE_SERIES + 6 val NHENTAI_SOURCE_ID = LEWD_SOURCE_SERIES + 7 +val HENTAI_CAFE_SOURCE_ID = LEWD_SOURCE_SERIES + 8 + fun isLewdSource(source: Long) = source in 6900..6999 fun isEhSource(source: Long) = source == EH_SOURCE_ID diff --git a/app/src/main/java/exh/GalleryAdder.kt b/app/src/main/java/exh/GalleryAdder.kt index 71c8f77ad..ab44c8ee7 100755 --- a/app/src/main/java/exh/GalleryAdder.kt +++ b/app/src/main/java/exh/GalleryAdder.kt @@ -10,19 +10,16 @@ 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.copyTo -import exh.metadata.loadEh -import exh.metadata.loadNhentai import exh.metadata.models.ExGalleryMetadata import exh.metadata.models.NHentaiMetadata +import exh.metadata.models.PervEdenGalleryMetadata +import exh.metadata.models.PervEdenLang import exh.util.defRealm -import io.realm.Realm import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import timber.log.Timber import uy.kohesive.injekt.injectLazy -import java.net.MalformedURLException import java.net.URI import java.net.URISyntaxException @@ -70,10 +67,19 @@ class GalleryAdder { forceSource: Long? = null): GalleryAddEvent { try { val urlObj = Uri.parse(url) - val source = when (urlObj.host) { + val lowercasePs = urlObj.pathSegments.map(String::toLowerCase) + val firstPathSegment = lowercasePs[0] + val source = when (urlObj.host.toLowerCase()) { "g.e-hentai.org", "e-hentai.org" -> EH_SOURCE_ID "exhentai.org" -> EXH_SOURCE_ID "nhentai.net" -> NHENTAI_SOURCE_ID + "www.perveden.com" -> { + when(lowercasePs[1]) { + "en-manga" -> PERV_EDEN_EN_SOURCE_ID + "it-manga" -> PERV_EDEN_IT_SOURCE_ID + else -> return GalleryAddEvent.Fail.UnknownType(url) + } + } else -> return GalleryAddEvent.Fail.UnknownType(url) } @@ -81,7 +87,6 @@ class GalleryAdder { return GalleryAddEvent.Fail.UnknownType(url) } - val firstPathSegment = urlObj.pathSegments.firstOrNull()?.toLowerCase() val realUrl = when(source) { EH_SOURCE_ID, EXH_SOURCE_ID -> when (firstPathSegment) { "g" -> { @@ -94,10 +99,19 @@ class GalleryAdder { } else -> return GalleryAddEvent.Fail.UnknownType(url) } - NHENTAI_SOURCE_ID -> when { - firstPathSegment == "g" -> url - urlObj.pathSegments.size >= 3 -> "https://nhentai.net/g/${urlObj.pathSegments[1]}/" - else -> return GalleryAddEvent.Fail.UnknownType(url) + NHENTAI_SOURCE_ID -> { + if(firstPathSegment != "g") + return GalleryAddEvent.Fail.UnknownType(url) + + "https://nhentai.net/g/${urlObj.pathSegments[1]}/" + } + PERV_EDEN_EN_SOURCE_ID, + PERV_EDEN_IT_SOURCE_ID -> { + val uri = Uri.parse("http://www.perveden.com/").buildUpon() + urlObj.pathSegments.take(3).forEach { + uri.appendPath(it) + } + uri.toString() } else -> return GalleryAddEvent.Fail.UnknownType(url) } @@ -108,6 +122,8 @@ class GalleryAdder { val cleanedUrl = when(source) { EH_SOURCE_ID, EXH_SOURCE_ID -> getUrlWithoutDomain(realUrl) NHENTAI_SOURCE_ID -> realUrl //nhentai uses URLs directly (oops, my bad when implementing this source) + PERV_EDEN_EN_SOURCE_ID, + PERV_EDEN_IT_SOURCE_ID -> getUrlWithoutDomain(realUrl) else -> return GalleryAddEvent.Fail.UnknownType(url) } @@ -119,17 +135,27 @@ class GalleryAdder { } //Copy basics - manga.copyFrom(sourceObj.fetchMangaDetails(manga).toBlocking().first()) + val newManga = sourceObj.fetchMangaDetails(manga).toBlocking().first() + manga.copyFrom(newManga) + manga.title = newManga.title //Forcibly copy title as copyFrom does not copy title //Apply metadata defRealm { realm -> when (source) { EH_SOURCE_ID, EXH_SOURCE_ID -> - realm.loadEh(ExGalleryMetadata.galleryId(realUrl), - ExGalleryMetadata.galleryToken(realUrl), - isExSource(source))?.copyTo(manga) + ExGalleryMetadata.UrlQuery(realUrl, isExSource(source)) + .query(realm) + .findFirst()?.copyTo(manga) NHENTAI_SOURCE_ID -> - realm.loadNhentai(NHentaiMetadata.nhIdFromUrl(realUrl)) + NHentaiMetadata.UrlQuery(realUrl) + .query(realm) + .findFirst() + ?.copyTo(manga) + PERV_EDEN_EN_SOURCE_ID, + PERV_EDEN_IT_SOURCE_ID -> + PervEdenGalleryMetadata.UrlQuery(realUrl, PervEdenLang.source(source)) + .query(realm) + .findFirst() ?.copyTo(manga) else -> return GalleryAddEvent.Fail.UnknownType(url) } @@ -160,16 +186,16 @@ class GalleryAdder { } private fun getUrlWithoutDomain(orig: String): String { - try { + return try { val uri = URI(orig) var out = uri.path if (uri.query != null) out += "?" + uri.query if (uri.fragment != null) out += "#" + uri.fragment - return out + out } catch (e: URISyntaxException) { - return orig + orig } } } diff --git a/app/src/main/java/exh/metadata/MetadataHelper.kt b/app/src/main/java/exh/metadata/MetadataHelper.kt index 25cce0921..10bc97d28 100755 --- a/app/src/main/java/exh/metadata/MetadataHelper.kt +++ b/app/src/main/java/exh/metadata/MetadataHelper.kt @@ -1,121 +1,32 @@ package exh.metadata import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.ui.library.LibraryItem import exh.* -import exh.metadata.models.ExGalleryMetadata -import exh.metadata.models.NHentaiMetadata -import exh.metadata.models.PervEdenGalleryMetadata -import exh.metadata.models.SearchableGalleryMetadata +import exh.metadata.models.* import io.realm.Realm import io.realm.RealmQuery import io.realm.RealmResults -import rx.Observable import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import kotlin.reflect.KClass -fun Realm.ehMetaQueryFromUrl(url: String, - exh: Boolean, - meta: RealmQuery? = null) = - ehMetadataQuery( - ExGalleryMetadata.galleryId(url), - ExGalleryMetadata.galleryToken(url), - exh, - meta - ) - -fun Realm.ehMetadataQuery(gId: String, - gToken: String, - exh: Boolean, - meta: RealmQuery? = null) - = (meta ?: where(ExGalleryMetadata::class.java)) - .equalTo(ExGalleryMetadata::gId.name, gId) - .equalTo(ExGalleryMetadata::gToken.name, gToken) - .equalTo(ExGalleryMetadata::exh.name, exh) - -fun Realm.loadEh(gId: String, gToken: String, exh: Boolean): ExGalleryMetadata? - = ehMetadataQuery(gId, gToken, exh) - .findFirst() - -fun Realm.loadEhAsync(gId: String, gToken: String, exh: Boolean): Observable - = ehMetadataQuery(gId, gToken, exh) - .findFirstAsync() - .asObservable() - -private fun pervEdenSourceToLang(source: Long) - = when (source) { - PERV_EDEN_EN_SOURCE_ID -> "en" - PERV_EDEN_IT_SOURCE_ID -> "it" - else -> throw IllegalArgumentException() -} - -fun Realm.pervEdenMetaQueryFromUrl(url: String, - source: Long, - meta: RealmQuery? = null) = - pervEdenMetadataQuery( - PervEdenGalleryMetadata.pvIdFromUrl(url), - source, - meta - ) - -fun Realm.pervEdenMetadataQuery(pvId: String, - source: Long, - meta: RealmQuery? = null) - = (meta ?: where(PervEdenGalleryMetadata::class.java)) - .equalTo(PervEdenGalleryMetadata::lang.name, pervEdenSourceToLang(source)) - .equalTo(PervEdenGalleryMetadata::pvId.name, pvId) - -fun Realm.loadPervEden(pvId: String, source: Long): PervEdenGalleryMetadata? - = pervEdenMetadataQuery(pvId, source) - .findFirst() - -fun Realm.loadPervEdenAsync(pvId: String, source: Long): Observable - = pervEdenMetadataQuery(pvId, source) - .findFirstAsync() - .asObservable() - -fun Realm.nhentaiMetaQueryFromUrl(url: String, - meta: RealmQuery? = null) = - nhentaiMetadataQuery( - NHentaiMetadata.nhIdFromUrl(url), - meta - ) - -fun Realm.nhentaiMetadataQuery(nhId: Long, - meta: RealmQuery? = null) - = (meta ?: where(NHentaiMetadata::class.java)) - .equalTo(NHentaiMetadata::nhId.name, nhId) - -fun Realm.loadNhentai(nhId: Long): NHentaiMetadata? - = nhentaiMetadataQuery(nhId) - .findFirst() - -fun Realm.loadNhentaiAsync(nhId: Long): Observable - = nhentaiMetadataQuery(nhId) - .findFirstAsync() - .asObservable() - fun Realm.loadAllMetadata(): Map, RealmResults> = - listOf, RealmQuery>>( - Pair(ExGalleryMetadata::class, where(ExGalleryMetadata::class.java)), - Pair(NHentaiMetadata::class, where(NHentaiMetadata::class.java)), - Pair(PervEdenGalleryMetadata::class, where(PervEdenGalleryMetadata::class.java)) - ).map { - Pair(it.first, it.second.findAllSorted(SearchableGalleryMetadata::mangaId.name)) + Injekt.get().getOnlineSources().filterIsInstance>().map { + it.queryAll() + }.associate { + it.clazz to it.query(this@loadAllMetadata).findAllSorted(SearchableGalleryMetadata::mangaId.name) }.toMap() fun Realm.queryMetadataFromManga(manga: Manga, - meta: RealmQuery? = null): RealmQuery = - when(manga.source) { - EH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, false, meta as? RealmQuery) - EXH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, true, meta as? RealmQuery) - PERV_EDEN_EN_SOURCE_ID, - PERV_EDEN_IT_SOURCE_ID -> - pervEdenMetaQueryFromUrl(manga.url, manga.source, meta as? RealmQuery) - NHENTAI_SOURCE_ID -> nhentaiMetaQueryFromUrl(manga.url, meta as? RealmQuery) - else -> throw IllegalArgumentException("Unknown source type!") - } + meta: RealmQuery? = null): + RealmQuery = + Injekt.get().get(manga.source)?.let { + (it as LewdSource<*, *>).queryFromUrl(manga.url) as GalleryQuery + }?.query(this, meta) ?: throw IllegalArgumentException("Unknown source type!") fun Realm.syncMangaIds(mangas: List) { Timber.d("--> EH: Begin syncing ${mangas.size} manga IDs...") @@ -138,11 +49,4 @@ fun Realm.syncMangaIds(mangas: List) { } val Manga.metadataClass - get() = when (source) { - EH_SOURCE_ID, - EXH_SOURCE_ID -> ExGalleryMetadata::class - PERV_EDEN_IT_SOURCE_ID, - PERV_EDEN_EN_SOURCE_ID -> PervEdenGalleryMetadata::class - NHENTAI_SOURCE_ID -> NHentaiMetadata::class - else -> null - } + get() = (Injekt.get().get(source) as? LewdSource<*, *>)?.queryAll()?.clazz diff --git a/app/src/main/java/exh/metadata/MetadataUtil.kt b/app/src/main/java/exh/metadata/MetadataUtil.kt index 73a205ea1..6acb2eb24 100755 --- a/app/src/main/java/exh/metadata/MetadataUtil.kt +++ b/app/src/main/java/exh/metadata/MetadataUtil.kt @@ -1,5 +1,10 @@ package exh.metadata +import exh.metadata.models.SearchableGalleryMetadata +import exh.plusAssign +import java.text.SimpleDateFormat +import java.util.* + /** * Metadata utils */ @@ -44,4 +49,37 @@ fun ignore(expr: () -> T): T? { fun Set>.forEach(action: (K, V) -> Unit) { forEach { action(it.key, it.value) } -} \ No newline at end of file +} + +val ONGOING_SUFFIX = arrayOf( + "[ongoing]", + "(ongoing)", + "{ongoing}", + "", + "ongoing", + "[incomplete]", + "(incomplete)", + "{incomplete}", + "", + "incomplete", + "[wip]", + "(wip)", + "{wip}", + "", + "wip" +) + +val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US) + +fun buildTagsDescription(metadata: SearchableGalleryMetadata) + = StringBuilder("Tags:\n").apply { + //BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags' + metadata.tags.groupBy { + it.namespace + }.entries.forEach { namespace, tags -> + if (tags.isNotEmpty()) { + val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" }) + this += "▪ $namespace: $joinedTags\n" + } + } +} diff --git a/app/src/main/java/exh/metadata/MetdataCopier.kt b/app/src/main/java/exh/metadata/MetdataCopier.kt deleted file mode 100755 index 5417f34d6..000000000 --- a/app/src/main/java/exh/metadata/MetdataCopier.kt +++ /dev/null @@ -1,219 +0,0 @@ -package exh.metadata - -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.PervEden -import exh.metadata.models.* -import exh.plusAssign -import uy.kohesive.injekt.injectLazy -import java.text.SimpleDateFormat -import java.util.* - -/** - * Copies gallery metadata to a manga object - */ - -private const val EH_ARTIST_NAMESPACE = "artist" -private const val EH_AUTHOR_NAMESPACE = "author" - -private const val NHENTAI_ARTIST_NAMESPACE = "artist" -private const val NHENTAI_CATEGORIES_NAMESPACE = "category" - -private val ONGOING_SUFFIX = arrayOf( - "[ongoing]", - "(ongoing)", - "{ongoing}" -) - -val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US) - -private val prefs: PreferencesHelper by injectLazy() - -fun ExGalleryMetadata.copyTo(manga: SManga) { - //TODO Find some way to do this with SManga - /*exh?.let { - manga.source = if(it) - 2 - else - 1 - }*/ - url?.let { manga.url = it } - thumbnailUrl?.let { manga.thumbnail_url = it } - - //No title bug? - val titleObj = if(prefs.useJapaneseTitle().getOrDefault()) - altTitle ?: title - else - title - titleObj?.let { manga.title = it } - - //Set artist (if we can find one) - 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.filter { it.namespace == EH_AUTHOR_NAMESPACE }.let { - if(it.isNotEmpty()) manga.author = it.joinToString(transform = { it.name!! }) - } - //Set genre - genre?.let { manga.genre = it } - - //Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes - //We default to completed - manga.status = SManga.COMPLETED - title?.let { t -> - ONGOING_SUFFIX.find { - t.endsWith(it, ignoreCase = true) - }?.let { - manga.status = SManga.ONGOING - } - } - - //Build a nice looking description out of what we know - val titleDesc = StringBuilder() - title?.let { titleDesc += "Title: $it\n" } - altTitle?.let { titleDesc += "Alternate Title: $it\n" } - - val detailsDesc = StringBuilder() - uploader?.let { detailsDesc += "Uploader: $it\n" } - datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" } - visible?.let { detailsDesc += "Visible: $it\n" } - language?.let { - detailsDesc += "Language: $it" - if(translated == true) detailsDesc += " TR" - detailsDesc += "\n" - } - size?.let { detailsDesc += "File Size: ${humanReadableByteCount(it, true)}\n" } - length?.let { detailsDesc += "Length: $it pages\n" } - favorites?.let { detailsDesc += "Favorited: $it times\n" } - averageRating?.let { - detailsDesc += "Rating: $it" - ratingCount?.let { detailsDesc += " ($it)" } - detailsDesc += "\n" - } - - val tagsDesc = buildTagsDescription(this) - - manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) - .filter(String::isNotBlank) - .joinToString(separator = "\n") -} - -fun PervEdenGalleryMetadata.copyTo(manga: SManga) { - url?.let { manga.url = it } - thumbnailUrl?.let { manga.thumbnail_url = it } - - val titleDesc = StringBuilder() - title?.let { - manga.title = it - titleDesc += "Title: $it\n" - } - if(altTitles.isNotEmpty()) - titleDesc += "Alternate Titles: \n" + altTitles.map { - "▪ ${it.title}" - }.joinToString(separator = "\n", postfix = "\n") - - val detailsDesc = StringBuilder() - artist?.let { - manga.artist = it - detailsDesc += "Artist: $it\n" - } - - type?.let { - manga.genre = it - detailsDesc += "Type: $it\n" - } - - status?.let { - manga.status = when(it) { - "Ongoing" -> SManga.ONGOING - "Completed", "Suspended" -> SManga.COMPLETED - else -> SManga.UNKNOWN - } - detailsDesc += "Status: $it\n" - } - - rating?.let { - detailsDesc += "Rating: %.2\n".format(it) - } - - val tagsDesc = buildTagsDescription(this) - - manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) - .filter(String::isNotBlank) - .joinToString(separator = "\n") -} - -fun NHentaiMetadata.copyTo(manga: SManga) { - url?.let { manga.url = it } - - //TODO next update allow this to be changed to use HD covers - if(mediaId != null) - NHentaiMetadata.typeToExtension(thumbnailImageType)?.let { - manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/thumb.$it" - } - - manga.title = englishTitle ?: japaneseTitle ?: shortTitle!! - - //Set artist (if we can find one) - tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let { - if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.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 - //We default to completed - manga.status = SManga.COMPLETED - englishTitle?.let { t -> - ONGOING_SUFFIX.find { - t.endsWith(it, ignoreCase = true) - }?.let { - manga.status = SManga.ONGOING - } - } - - val titleDesc = StringBuilder() - englishTitle?.let { titleDesc += "English Title: $it\n" } - japaneseTitle?.let { titleDesc += "Japanese Title: $it\n" } - shortTitle?.let { titleDesc += "Short Title: $it\n" } - - val detailsDesc = StringBuilder() - uploadDate?.let { detailsDesc += "Upload Date: ${EX_DATE_FORMAT.format(Date(it))}\n" } - pageImageTypes.size.let { detailsDesc += "Length: $it pages\n" } - favoritesCount?.let { detailsDesc += "Favorited: $it times\n" } - scanlator?.nullIfBlank()?.let { detailsDesc += "Scanlator: $it\n" } - - val tagsDesc = buildTagsDescription(this) - - manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) - .filter(String::isNotBlank) - .joinToString(separator = "\n") -} - -fun SearchableGalleryMetadata.genericCopyTo(manga: SManga): Boolean { - when(this) { - is ExGalleryMetadata -> this.copyTo(manga) - is PervEdenGalleryMetadata -> this.copyTo(manga) - is NHentaiMetadata -> this.copyTo(manga) - else -> return false - } - return true -} - -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.groupBy { - it.namespace - }.entries.forEach { namespace, tags -> - if (tags.isNotEmpty()) { - val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" }) - this += "▪ $namespace: $joinedTags\n" - } - } - } diff --git a/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt b/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt index a2fdc08b3..b5c4da343 100755 --- a/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt +++ b/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt @@ -1,12 +1,22 @@ package exh.metadata.models import android.net.Uri +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.source.model.SManga +import exh.metadata.EX_DATE_FORMAT +import exh.metadata.ONGOING_SUFFIX +import exh.metadata.buildTagsDescription +import exh.metadata.humanReadableByteCount +import exh.plusAssign 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 uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.util.* /** @@ -61,12 +71,99 @@ open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata { @Index override var mangaId: Long? = null + class EmptyQuery : GalleryQuery(ExGalleryMetadata::class) + + class UrlQuery( + val url: String, + val exh: Boolean + ) : GalleryQuery(ExGalleryMetadata::class) { + override fun transform() = Query( + galleryId(url), + galleryToken(url), + exh + ) + } + + class Query(val gId: String, + val gToken: String, + val exh: Boolean + ) : GalleryQuery(ExGalleryMetadata::class) { + override fun map() = mapOf( + ExGalleryMetadata::gId to Query::gId, + ExGalleryMetadata::gToken to Query::gToken, + ExGalleryMetadata::exh to Query::exh + ) + } + + override fun copyTo(manga: SManga) { + url?.let { manga.url = it } + thumbnailUrl?.let { manga.thumbnail_url = it } + + //No title bug? + val titleObj = if(Injekt.get().useJapaneseTitle().getOrDefault()) + altTitle ?: title + else + title + titleObj?.let { manga.title = it } + + //Set artist (if we can find one) + 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.filter { it.namespace == EH_AUTHOR_NAMESPACE }.let { + if(it.isNotEmpty()) manga.author = it.joinToString(transform = { it.name!! }) + } + //Set genre + genre?.let { manga.genre = it } + + //Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes + //We default to completed + manga.status = SManga.COMPLETED + title?.let { t -> + ONGOING_SUFFIX.find { + t.endsWith(it, ignoreCase = true) + }?.let { + manga.status = SManga.ONGOING + } + } + + //Build a nice looking description out of what we know + val titleDesc = StringBuilder() + title?.let { titleDesc += "Title: $it\n" } + altTitle?.let { titleDesc += "Alternate Title: $it\n" } + + val detailsDesc = StringBuilder() + uploader?.let { detailsDesc += "Uploader: $it\n" } + datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" } + visible?.let { detailsDesc += "Visible: $it\n" } + language?.let { + detailsDesc += "Language: $it" + if(translated == true) detailsDesc += " TR" + detailsDesc += "\n" + } + size?.let { detailsDesc += "File Size: ${humanReadableByteCount(it, true)}\n" } + length?.let { detailsDesc += "Length: $it pages\n" } + favorites?.let { detailsDesc += "Favorited: $it times\n" } + averageRating?.let { + detailsDesc += "Rating: $it" + ratingCount?.let { detailsDesc += " ($it)" } + detailsDesc += "\n" + } + + val tagsDesc = buildTagsDescription(this) + + manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) + .filter(String::isNotBlank) + .joinToString(separator = "\n") + } + companion object { private fun splitGalleryUrl(url: String) = url.let { - Uri.parse(it).pathSegments - .filterNot(String::isNullOrBlank) - } + Uri.parse(it).pathSegments + .filterNot(String::isNullOrBlank) + } fun galleryId(url: String) = splitGalleryUrl(url).let { it[it.size - 2] } @@ -77,5 +174,9 @@ open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata { ExGalleryMetadata::title.name, ExGalleryMetadata::altTitle.name ) + + private const val EH_ARTIST_NAMESPACE = "artist" + private const val EH_AUTHOR_NAMESPACE = "author" + } } \ No newline at end of file diff --git a/app/src/main/java/exh/metadata/models/GalleryQuery.kt b/app/src/main/java/exh/metadata/models/GalleryQuery.kt new file mode 100755 index 000000000..521a5dbd7 --- /dev/null +++ b/app/src/main/java/exh/metadata/models/GalleryQuery.kt @@ -0,0 +1,68 @@ +package exh.metadata.models + +import io.realm.* +import java.util.* +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 + +abstract class GalleryQuery(val clazz: KClass) { + open fun map(): Map<*, *> = emptyMap, KProperty1, *>>() + + open fun transform(): GalleryQuery? = this + + open fun override(meta: RealmQuery): RealmQuery = meta + + fun query(realm: Realm, meta: RealmQuery? = null): RealmQuery + = (meta ?: realm.where(clazz.java)).let { + val visited = mutableListOf>() + + var top: GalleryQuery? = null + var newMeta = it + while(true) { + //DIFFERENT BEHAVIOR from: top?.transform() ?: this + top = if(top != null) top.transform() else this + + if(top == null) break + + if(top in visited) break + + newMeta = top.applyMap(newMeta) + newMeta = top.override(newMeta) + + visited += top + } + + newMeta + }!! + + fun applyMap(meta: RealmQuery): RealmQuery { + var newMeta = meta + + map().forEach { (t, u) -> + t as KProperty + u as KProperty1, *> + + val v = u.get(this) + val n = t.name + + if(v != null) { + newMeta = when (v) { + is Date -> newMeta.equalTo(n, v) + is Boolean -> newMeta.equalTo(n, v) + is Byte -> newMeta.equalTo(n, v) + is ByteArray -> newMeta.equalTo(n, v) + is Double -> newMeta.equalTo(n, v) + is Float -> newMeta.equalTo(n, v) + is Int -> newMeta.equalTo(n, v) + is Long -> newMeta.equalTo(n, v) + is Short -> newMeta.equalTo(n, v) + is String -> newMeta.equalTo(n, v, Case.INSENSITIVE) + else -> throw IllegalArgumentException("Unknown type: ${v::class.qualifiedName}!") + } + } + } + + return newMeta + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/metadata/models/HentaiCafeMetadata.kt b/app/src/main/java/exh/metadata/models/HentaiCafeMetadata.kt new file mode 100644 index 000000000..e7ec88130 --- /dev/null +++ b/app/src/main/java/exh/metadata/models/HentaiCafeMetadata.kt @@ -0,0 +1,91 @@ +package exh.metadata.models + +import eu.kanade.tachiyomi.source.model.SManga +import exh.metadata.buildTagsDescription +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 HentaiCafeMetadata : RealmObject(), SearchableGalleryMetadata { + @PrimaryKey + override var uuid: String = UUID.randomUUID().toString() + + @Index + var hcId: String? = null + var readerId: String? = null + + var url get() = hcId?.let { "$BASE_URL/$it" } + set(a) { + a?.let { + hcId = hcIdFromUrl(a) + } + } + + var title: String? = null + + var artist: String? = null + + override var uploader: String? = null + + override var tags: RealmList = RealmList() + + override fun getTitles() = listOf(title).filterNotNull() + + @Ignore + override val titleFields = listOf( + HentaiCafeMetadata::title.name + ) + + @Index + override var mangaId: Long? = null + + override fun copyTo(manga: SManga) { + manga.title = title!! + manga.artist = artist + manga.author = artist + + //Not available + manga.status = SManga.UNKNOWN + + val detailsDesc = "Title: $title\n" + + "Artist: $artist\n" + + val tagsDesc = buildTagsDescription(this) + + manga.genre = tags.filter { it.namespace == "tag" }.joinToString { + it.name!! + } + + manga.description = listOf(detailsDesc, tagsDesc.toString()) + .filter(String::isNotBlank) + .joinToString(separator = "\n") + } + + class EmptyQuery : GalleryQuery(HentaiCafeMetadata::class) + + class UrlQuery( + val url: String + ) : GalleryQuery(HentaiCafeMetadata::class) { + override fun transform() = Query( + hcIdFromUrl(url) + ) + } + + class Query(val hcId: String): GalleryQuery(HentaiCafeMetadata::class) { + override fun map() = mapOf( + HentaiCafeMetadata::hcId to Query::hcId + ) + } + + companion object { + val BASE_URL = "https://hentai.cafe" + + fun hcIdFromUrl(url: String) + = url.split("/").last { it.isNotBlank() } + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/metadata/models/NHentaiMetadata.kt b/app/src/main/java/exh/metadata/models/NHentaiMetadata.kt index fd5a6a0f3..6b301dbe8 100755 --- a/app/src/main/java/exh/metadata/models/NHentaiMetadata.kt +++ b/app/src/main/java/exh/metadata/models/NHentaiMetadata.kt @@ -1,11 +1,21 @@ package exh.metadata.models +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.source.model.SManga +import exh.metadata.EX_DATE_FORMAT +import exh.metadata.ONGOING_SUFFIX +import exh.metadata.buildTagsDescription +import exh.metadata.nullIfBlank +import exh.plusAssign 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 uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.util.* /** @@ -58,18 +68,92 @@ open class NHentaiMetadata : RealmObject(), SearchableGalleryMetadata { @Index override var mangaId: Long? = null + class EmptyQuery : GalleryQuery(NHentaiMetadata::class) + + class UrlQuery( + val url: String + ) : GalleryQuery(NHentaiMetadata::class) { + override fun transform() = Query( + nhIdFromUrl(url) + ) + } + + class Query( + val nhId: Long + ) : GalleryQuery(NHentaiMetadata::class) { + override fun map() = mapOf( + NHentaiMetadata::nhId to Query::nhId + ) + } + + override fun copyTo(manga: SManga) { + url?.let { manga.url = it } + + if(mediaId != null) + NHentaiMetadata.typeToExtension(thumbnailImageType)?.let { + manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${ + if(Injekt.get().eh_useHighQualityThumbs().getOrDefault()) + "cover" + else + "thumb" + }.$it" + } + + manga.title = englishTitle ?: japaneseTitle ?: shortTitle!! + + //Set artist (if we can find one) + tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let { + if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.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 + //We default to completed + manga.status = SManga.COMPLETED + englishTitle?.let { t -> + ONGOING_SUFFIX.find { + t.endsWith(it, ignoreCase = true) + }?.let { + manga.status = SManga.ONGOING + } + } + + val titleDesc = StringBuilder() + englishTitle?.let { titleDesc += "English Title: $it\n" } + japaneseTitle?.let { titleDesc += "Japanese Title: $it\n" } + shortTitle?.let { titleDesc += "Short Title: $it\n" } + + val detailsDesc = StringBuilder() + uploadDate?.let { detailsDesc += "Upload Date: ${EX_DATE_FORMAT.format(Date(it * 1000))}\n" } + pageImageTypes.size.let { detailsDesc += "Length: $it pages\n" } + favoritesCount?.let { detailsDesc += "Favorited: $it times\n" } + scanlator?.nullIfBlank()?.let { detailsDesc += "Scanlator: $it\n" } + + val tagsDesc = buildTagsDescription(this) + + manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) + .filter(String::isNotBlank) + .joinToString(separator = "\n") + } + companion object { val BASE_URL = "https://nhentai.net" + private const val NHENTAI_ARTIST_NAMESPACE = "artist" + private const val NHENTAI_CATEGORIES_NAMESPACE = "category" + fun typeToExtension(t: String?) = - when(t) { - "p" -> "png" - "j" -> "jpg" - else -> null - } + when(t) { + "p" -> "png" + "j" -> "jpg" + else -> null + } fun nhIdFromUrl(url: String) - = url.split("/").last { it.isNotBlank() }.toLong() + = url.split("/").last { it.isNotBlank() }.toLong() val TITLE_FIELDS = listOf( NHentaiMetadata::japaneseTitle.name, diff --git a/app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt b/app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt index f3942d7db..187e697b4 100755 --- a/app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt +++ b/app/src/main/java/exh/metadata/models/PervEdenGalleryMetadata.kt @@ -1,8 +1,14 @@ package exh.metadata.models import android.net.Uri +import eu.kanade.tachiyomi.source.model.SManga +import exh.PERV_EDEN_EN_SOURCE_ID +import exh.PERV_EDEN_IT_SOURCE_ID +import exh.metadata.buildTagsDescription +import exh.plusAssign import io.realm.RealmList import io.realm.RealmObject +import io.realm.RealmQuery import io.realm.annotations.Ignore import io.realm.annotations.Index import io.realm.annotations.PrimaryKey @@ -50,11 +56,79 @@ open class PervEdenGalleryMetadata : RealmObject(), SearchableGalleryMetadata { @Index override var mangaId: Long? = null + override fun copyTo(manga: SManga) { + url?.let { manga.url = it } + thumbnailUrl?.let { manga.thumbnail_url = it } + + val titleDesc = StringBuilder() + title?.let { + manga.title = it + titleDesc += "Title: $it\n" + } + if(altTitles.isNotEmpty()) + titleDesc += "Alternate Titles: \n" + altTitles.map { + "▪ ${it.title}" + }.joinToString(separator = "\n", postfix = "\n") + + val detailsDesc = StringBuilder() + artist?.let { + manga.artist = it + detailsDesc += "Artist: $it\n" + } + + type?.let { + manga.genre = it + detailsDesc += "Type: $it\n" + } + + status?.let { + manga.status = when(it) { + "Ongoing" -> SManga.ONGOING + "Completed", "Suspended" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + detailsDesc += "Status: $it\n" + } + + rating?.let { + detailsDesc += "Rating: %.2\n".format(it) + } + + val tagsDesc = buildTagsDescription(this) + + manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) + .filter(String::isNotBlank) + .joinToString(separator = "\n") + } + + class EmptyQuery : GalleryQuery(PervEdenGalleryMetadata::class) + + class UrlQuery( + val url: String, + val lang: PervEdenLang + ) : GalleryQuery(PervEdenGalleryMetadata::class) { + override fun transform() = Query( + pvIdFromUrl(url), + lang + ) + } + + class Query(val pvId: String, + val lang: PervEdenLang + ) : GalleryQuery(PervEdenGalleryMetadata::class) { + override fun map() = mapOf( + PervEdenGalleryMetadata::pvId to Query::pvId + ) + + override fun override(meta: RealmQuery) + = meta.equalTo(PervEdenGalleryMetadata::lang.name, lang.name) + } + companion object { private fun splitGalleryUrl(url: String) = url.let { - Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank) - } + Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank) + } fun pvIdFromUrl(url: String) = splitGalleryUrl(url).last() @@ -88,3 +162,14 @@ open class PervEdenTitle(var metadata: PervEdenGalleryMetadata? = null, override fun toString() = "PervEdenTitle(metadata=$metadata, title=$title)" } + +enum class PervEdenLang(val id: Long) { + en(PERV_EDEN_EN_SOURCE_ID), + it(PERV_EDEN_IT_SOURCE_ID); + + companion object { + fun source(id: Long) + = PervEdenLang.values().find { it.id == id } + ?: throw IllegalArgumentException("Unknown source ID: $id!") + } +} \ No newline at end of file diff --git a/app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt b/app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt index a3ab65c9f..c38fd7ee5 100755 --- a/app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt +++ b/app/src/main/java/exh/metadata/models/SearchableGalleryMetadata.kt @@ -1,11 +1,8 @@ package exh.metadata.models +import eu.kanade.tachiyomi.source.model.SManga 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 @@ -23,4 +20,6 @@ interface SearchableGalleryMetadata: RealmModel { val titleFields: List var mangaId: Long? + + fun copyTo(manga: SManga) } \ No newline at end of file diff --git a/app/src/main/java/exh/search/SearchEngine.kt b/app/src/main/java/exh/search/SearchEngine.kt index 55f98ec9f..227ed9bf3 100755 --- a/app/src/main/java/exh/search/SearchEngine.kt +++ b/app/src/main/java/exh/search/SearchEngine.kt @@ -18,12 +18,11 @@ class SearchEngine { fun matchTagList(namespace: String?, component: Text?, excluded: Boolean) { - if(excluded) - rQuery.not() - else if (queryEmpty) - queryEmpty = false - else - rQuery.or() + when { + excluded -> rQuery.not() + queryEmpty -> queryEmpty = false + else -> rQuery.or() + } rQuery.beginGroup() //Match namespace if specified diff --git a/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt b/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt index 5e9ee70fb..d98c0f99e 100755 --- a/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt +++ b/app/src/main/java/exh/ui/migration/MetadataFetchDialog.kt @@ -11,10 +11,8 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.SourceManager import exh.isExSource import exh.isLewdSource -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 @@ -64,7 +62,7 @@ class MetadataFetchDialog { val source = sourceManager.get(manga.source) source?.let { manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first()) - realm.queryMetadataFromManga(manga).findFirst()?.genericCopyTo(manga) + realm.queryMetadataFromManga(manga).findFirst()?.copyTo(manga) } } catch (t: Throwable) { Timber.e(t, "Could not migrate manga!") diff --git a/app/src/main/java/exh/ui/migration/UrlMigrator.kt b/app/src/main/java/exh/ui/migration/UrlMigrator.kt index 5719d6f56..3844928de 100755 --- a/app/src/main/java/exh/ui/migration/UrlMigrator.kt +++ b/app/src/main/java/exh/ui/migration/UrlMigrator.kt @@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import exh.isExSource import exh.isLewdSource -import exh.metadata.ehMetaQueryFromUrl +import exh.metadata.models.ExGalleryMetadata import exh.util.realmTrans import uy.kohesive.injekt.injectLazy @@ -43,7 +43,9 @@ class UrlMigrator { //Build fixed URL val urlWithSlash = "/" + manga.url //Fix metadata if required - val metadata = realm.ehMetaQueryFromUrl(manga.url, isExSource(manga.source)).findFirst() + val metadata = ExGalleryMetadata.UrlQuery(manga.url, isExSource(manga.source)) + .query(realm) + .findFirst() metadata?.url?.let { if (it.startsWith("g/")) { //Check if metadata URL has no slash metadata.url = urlWithSlash //Fix it diff --git a/app/src/main/java/exh/util/LoggingRealmQuery.kt b/app/src/main/java/exh/util/LoggingRealmQuery.kt index 6b88e59c9..ca02edfc1 100644 --- a/app/src/main/java/exh/util/LoggingRealmQuery.kt +++ b/app/src/main/java/exh/util/LoggingRealmQuery.kt @@ -480,19 +480,19 @@ class LoggingRealmQuery(val query: RealmQuery) { return query.average(fieldName) } - fun min(fieldName: String): Number { + fun min(fieldName: String): Number? { return query.min(fieldName) } - fun minimumDate(fieldName: String): Date { + fun minimumDate(fieldName: String): Date? { return query.minimumDate(fieldName) } - fun max(fieldName: String): Number { + fun max(fieldName: String): Number? { return query.max(fieldName) } - fun maximumDate(fieldName: String): Date { + fun maximumDate(fieldName: String): Date? { return query.maximumDate(fieldName) } @@ -540,7 +540,7 @@ class LoggingRealmQuery(val query: RealmQuery) { return query.findAllSortedAsync(fieldName1, sortOrder1, fieldName2, sortOrder2) } - fun findFirst(): E { + fun findFirst(): E? { return query.findFirst() } diff --git a/app/src/main/java/exh/util/RealmUtil.kt b/app/src/main/java/exh/util/RealmUtil.kt index f794560c3..4b8e70d3f 100644 --- a/app/src/main/java/exh/util/RealmUtil.kt +++ b/app/src/main/java/exh/util/RealmUtil.kt @@ -7,24 +7,8 @@ import java.util.* inline fun realmTrans(block: (Realm) -> T): T { return defRealm { - it.beginTransaction() - try { - val res = block(it) - it.commitTransaction() - res - } catch(t: Throwable) { - if (it.isInTransaction) { - it.cancelTransaction() - } else { - RealmLog.warn("Could not cancel transaction, not currently in a transaction.") - } - - throw t - } finally { - //Just in case - if (it.isInTransaction) { - it.cancelTransaction() - } + it.trans { + block(it) } } } @@ -35,5 +19,27 @@ inline fun defRealm(block: (Realm) -> T): T { } } +inline fun Realm.trans(block: () -> T): T { + beginTransaction() + try { + val res = block() + commitTransaction() + return res + } catch(t: Throwable) { + if (isInTransaction) { + cancelTransaction() + } else { + RealmLog.warn("Could not cancel transaction, not currently in a transaction.") + } + + throw t + } finally { + //Just in case + if (isInTransaction) { + cancelTransaction() + } + } +} + fun Realm.createUUIDObj(clazz: Class) - = createObject(clazz, UUID.randomUUID().toString()) + = createObject(clazz, UUID.randomUUID().toString())!! diff --git a/app/src/main/res/drawable/eh_ic_nhlogo_color.xml b/app/src/main/res/drawable/eh_ic_nhlogo_color.xml new file mode 100644 index 000000000..85942d927 --- /dev/null +++ b/app/src/main/res/drawable/eh_ic_nhlogo_color.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e94e46e8..f1bcdc179 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -451,4 +451,5 @@ Login E-Hentai + nhentai