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