Various changes

This commit is contained in:
NerdNumber9 2017-11-29 20:35:10 -05:00 committed by NerdNumber9
parent 908128b55d
commit 5cb219d83e
35 changed files with 1140 additions and 649 deletions

3
.gitignore vendored
View File

@ -7,5 +7,6 @@
*iml *iml
*.iml *.iml
*/build */build
/mainframer.sh /mainframer
/.mainframer
*.apk *.apk

6
CHANGELOG.md Normal file
View File

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

View File

@ -35,10 +35,6 @@ android {
buildToolsVersion "26.0.2" buildToolsVersion "26.0.2"
publishNonDefault true publishNonDefault true
dexOptions {
javaMaxHeapSize "4g"
}
defaultConfig { defaultConfig {
applicationId "eu.kanade.tachiyomi.eh2" applicationId "eu.kanade.tachiyomi.eh2"
minSdkVersion 16 minSdkVersion 16
@ -96,6 +92,9 @@ android {
exclude 'META-INF/LICENSE' exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt' exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE' exclude 'META-INF/NOTICE'
// Compatibility for two RxJava versions (EXH)
exclude 'META-INF/rxjava.properties'
} }
lintOptions { lintOptions {
@ -237,17 +236,17 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
//Pin lock view (EXH) //Pin lock view (EXH)
compile 'com.andrognito.pinlockview:pinlockview:2.1.0' implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
//Reprint (EXH) //Reprint (EXH)
compile 'com.github.ajalt.reprint:core:3.2.0@aar' // required: supports marshmallow devices implementation '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:rxjava:3.2.0@aar' // optional: the RxJava 1 interface
//Swirl (EXH) //Swirl (EXH)
compile 'com.mattprecious.swirl:swirl:1.0.0' implementation 'com.mattprecious.swirl:swirl:1.0.0'
//RxJava 2 interop for Realm (EXH) //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 { buildscript {

View File

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

View File

@ -117,4 +117,5 @@ object PreferenceKeys {
fun trackToken(syncId: Int) = "track_token_$syncId" fun trackToken(syncId: Int) = "track_token_$syncId"
const val eh_nh_useHighQualityThumbs = "eh_nh_hq_thumbs"
} }

View File

@ -201,5 +201,7 @@ class PreferencesHelper(val context: Context) {
fun lockLength() = rxPrefs.getInteger("lock_length", -1) fun lockLength() = rxPrefs.getInteger("lock_length", -1)
fun lockUseFingerprint() = rxPrefs.getBoolean("lock_finger", false) fun lockUseFingerprint() = rxPrefs.getBoolean("lock_finger", false)
fun eh_useHighQualityThumbs() = rxPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false)
// <-- EH // <-- EH
} }

View File

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.source.online.russian.Mintmanga
import eu.kanade.tachiyomi.source.online.russian.Readmanga import eu.kanade.tachiyomi.source.online.russian.Readmanga
import eu.kanade.tachiyomi.util.hasPermission import eu.kanade.tachiyomi.util.hasPermission
import exh.* import exh.*
import exh.metadata.models.PervEdenLang
import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.Yaml
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
@ -93,9 +94,10 @@ open class SourceManager(private val context: Context) {
if(prefs.enableExhentai().getOrDefault()) { if(prefs.enableExhentai().getOrDefault()) {
exSrcs += EHentai(EXH_SOURCE_ID, true, context) exSrcs += EHentai(EXH_SOURCE_ID, true, context)
} }
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, "en") exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en)
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, "it") exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it)
exSrcs += NHentai(context) exSrcs += NHentai(context)
exSrcs += HentaiCafe()
return exSrcs return exSrcs
} }

View File

@ -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<M : SearchableGalleryMetadata, I> : CatalogueSource {
fun queryAll(): GalleryQuery<M>
fun queryFromUrl(url: String): GalleryQuery<M>
val metaParser: M.(I) -> Unit
fun parseToManga(query: GalleryQuery<M>, 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<M>, parserInput: Observable<I>): Observable<M> {
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))
}
}
}

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.metadata.* import exh.metadata.*
import exh.metadata.models.ExGalleryMetadata import exh.metadata.models.ExGalleryMetadata
@ -24,13 +25,11 @@ import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import exh.GalleryAdder
import exh.util.* import exh.util.*
import io.realm.Realm
class EHentai(override val id: Long, class EHentai(override val id: Long,
val exh: Boolean, val exh: Boolean,
val context: Context) : HttpSource() { val context: Context) : HttpSource(), LewdSource<ExGalleryMetadata, Response> {
val schema: String val schema: String
get() = if(prefs.secureEXH().getOrDefault()) get() = if(prefs.secureEXH().getOrDefault())
@ -49,8 +48,6 @@ class EHentai(override val id: Long,
val prefs: PreferencesHelper by injectLazy() val prefs: PreferencesHelper by injectLazy()
val galleryAdder = GalleryAdder()
/** /**
* Gallery list entry * Gallery list entry
*/ */
@ -185,90 +182,80 @@ class EHentai(override val id: Long,
/** /**
* Parse gallery page to metadata model * Parse gallery page to metadata model
*/ */
override fun mangaDetailsParse(response: Response) override fun mangaDetailsParse(response: Response): SManga {
= with(response.asJsoup()) { return parseToManga(queryFromUrl(response.request().url().toString()), response)
realmTrans { realm -> }
val url = response.request().url().encodedPath()!!
val gId = ExGalleryMetadata.galleryId(url)
val gToken = ExGalleryMetadata.galleryToken(url)
val metdata = (realm.loadEh(gId, gToken, exh) override val metaParser: ExGalleryMetadata.(Response) -> Unit = { response ->
?: realm.createUUIDObj(ExGalleryMetadata::class.java)) with(response.asJsoup()) {
with(metdata) { url = response.request().url().encodedPath()!!
this.url = url gId = ExGalleryMetadata.galleryId(url!!)
this.gId = gId gToken = ExGalleryMetadata.galleryToken(url!!)
this.gToken = gToken
exh = this@EHentai.exh exh = this@EHentai.exh
title = select("#gn").text().nullIfBlank()?.trim() 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 { thumbnailUrl = select("#gd1 div").attr("style").nullIfBlank()?.let {
it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')')) it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
} }
genre = select(".ic").parents().attr("href").nullIfBlank()?.trim()?.substringAfterLast('/') genre = select(".ic").parents().attr("href").nullIfBlank()?.trim()?.substringAfterLast('/')
uploader = select("#gdn").text().nullIfBlank()?.trim() uploader = select("#gdn").text().nullIfBlank()?.trim()
//Parse the table //Parse the table
select("#gdd tr").forEach { select("#gdd tr").forEach {
it.select(".gdt1") it.select(".gdt1")
.text() .text()
.nullIfBlank() .nullIfBlank()
?.trim() ?.trim()
?.let { left -> ?.let { left ->
it.select(".gdt2") it.select(".gdt2")
.text() .text()
.nullIfBlank() .nullIfBlank()
?.trim() ?.trim()
?.let { right -> ?.let { right ->
ignore { ignore {
when (left.removeSuffix(":") when (left.removeSuffix(":")
.toLowerCase()) { .toLowerCase()) {
"posted" -> datePosted = EX_DATE_FORMAT.parse(right).time "posted" -> datePosted = EX_DATE_FORMAT.parse(right).time
"visible" -> visible = right.nullIfBlank() "visible" -> visible = right.nullIfBlank()
"language" -> { "language" -> {
language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank() language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank()
translated = right.endsWith(TR_SUFFIX, true) translated = right.endsWith(TR_SUFFIX, true)
}
"file size" -> size = parseHumanReadableByteCount(right)?.toLong()
"length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
"favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
} }
"file size" -> size = parseHumanReadableByteCount(right)?.toLong()
"length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
"favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
} }
} }
} }
} }
}
//Parse ratings //Parse ratings
ignore { ignore {
averageRating = select("#rating_label") averageRating = select("#rating_label")
.text() .text()
.removePrefix("Average:") .removePrefix("Average:")
.trim() .trim()
.nullIfBlank() .nullIfBlank()
?.toDouble() ?.toDouble()
ratingCount = select("#rating_count") ratingCount = select("#rating_count")
.text() .text()
.trim() .trim()
.nullIfBlank() .nullIfBlank()
?.toInt() ?.toInt()
} }
//Parse tags //Parse tags
tags.clear() tags.clear()
select("#taglist tr").forEach { select("#taglist tr").forEach {
val namespace = it.select(".tc").text().removeSuffix(":") val namespace = it.select(".tc").text().removeSuffix(":")
tags.addAll(it.select("div").map { tags.addAll(it.select("div").map {
Tag(namespace, it.text().trim(), it.hasClass("gtl")) Tag(namespace, it.text().trim(), it.hasClass("gtl"))
}) })
}
//Copy metadata to manga
SManga.create().apply {
copyTo(this)
}
} }
} }
} }
@ -323,7 +310,7 @@ class EHentai(override val id: Long,
if (favNames == null) if (favNames == null)
favNames = doc.getElementsByClass("nosel").first().children().filter { favNames = doc.getElementsByClass("nosel").first().children().filter {
it.children().size >= 3 it.children().size >= 3
}.map { it.child(2).text() }.filterNotNull() }.mapNotNull { it.child(2).text() }
//Next page //Next page
page++ page++
@ -384,9 +371,9 @@ class EHentai(override val id: Long,
} }
fun buildCookies(cookies: Map<String, String>) fun buildCookies(cookies: Map<String, String>)
= cookies.entries.map { = cookies.entries.joinToString(separator = "; ", postfix = ";") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}" "${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}.joinToString(separator = "; ", postfix = ";") }
fun addParam(url: String, param: String, value: String) fun addParam(url: String, param: String, value: String)
= Uri.parse(url) = Uri.parse(url)
@ -465,6 +452,9 @@ class EHentai(override val id: Long,
else else
"E-Hentai" "E-Hentai"
override fun queryAll() = ExGalleryMetadata.EmptyQuery()
override fun queryFromUrl(url: String) = ExGalleryMetadata.UrlQuery(url, exh)
companion object { companion object {
val QUERY_PREFIX = "?f_apply=Apply+Filter" val QUERY_PREFIX = "?f_apply=Apply+Filter"
val TR_SUFFIX = "TR" val TR_SUFFIX = "TR"

View File

@ -2,10 +2,7 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.github.salomonbrys.kotson.get import com.github.salomonbrys.kotson.*
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.long
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject 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.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import exh.NHENTAI_SOURCE_ID 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.NHentaiMetadata
import exh.metadata.models.PageImageType import exh.metadata.models.PageImageType
import exh.metadata.models.Tag import exh.metadata.models.Tag
import exh.util.createUUIDObj import exh.util.*
import exh.util.defRealm
import exh.util.realmTrans
import exh.util.urlImportFetchSearchManga
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
@ -36,7 +28,7 @@ import timber.log.Timber
* NHentai source * NHentai source
*/ */
class NHentai(context: Context) : HttpSource() { class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiMetadata, JsonObject> {
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> {
//TODO There is currently no way to get the most popular mangas //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 //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) override fun latestUpdatesParse(response: Response)
= parseResultPage(response) = parseResultPage(response)
override fun mangaDetailsParse(response: Response) override fun mangaDetailsParse(response: Response): SManga {
= parseGallery(jsonParser.parse(response.body()!!.string()).asJsonObject) 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 //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<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
@ -102,7 +96,8 @@ class NHentai(context: Context) : HttpSource() {
val error = res.get("error") val error = res.get("error")
if(error == null) { if(error == null) {
val results = res.getAsJsonArray("result")?.map { 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 val numPages = res.get("num_pages")?.int
if(results != null && numPages != null) if(results != null && numPages != null)
@ -113,70 +108,65 @@ class NHentai(context: Context) : HttpSource() {
return MangasPage(emptyList(), false) return MangasPage(emptyList(), false)
} }
fun rawParseGallery(obj: JsonObject) = realmTrans { realm -> override val metaParser: NHentaiMetadata.(JsonObject) -> Unit = { obj ->
val nhId = obj.get("id").asLong nhId = obj["id"].asLong
realm.copyFromRealm((realm.loadNhentai(nhId) uploadDate = obj["upload_date"].nullLong
?: realm.createUUIDObj(NHentaiMetadata::class.java)).apply {
this.nhId = nhId
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 { obj["images"].nullObj?.let {
japaneseTitle = it.get("japanese")?.notNull()?.string coverImageType = it["cover"]?.get("t").nullString
shortTitle = it.get("pretty")?.notNull()?.string it["pages"].nullArray?.mapNotNull {
englishTitle = it.get("english")?.notNull()?.string 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 { scanlator = obj["scanlator"].nullString
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.get("scanlator")?.notNull()?.asString obj["tags"]?.asJsonArray?.map {
val asObj = it.asJsonObject
obj.get("tags")?.asJsonArray?.map { Pair(asObj["type"].nullString, asObj["name"].nullString)
val asObj = it.asJsonObject }?.apply {
Pair(asObj.get("type")?.string, asObj.get("name")?.string) tags.clear()
}?.apply { }?.forEach {
tags.clear() if(it.first != null && it.second != null)
}?.forEach { tags.add(Tag(it.first!!, it.second!!, false))
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)
} }
} }
fun lazyLoadMetadata(url: String) = fun lazyLoadMetadata(url: String) =
defRealm { realm -> defRealm { realm ->
val meta = realm.loadNhentai(NHentaiMetadata.nhIdFromUrl(url)) val meta = NHentaiMetadata.UrlQuery(url).query(realm).findFirst()
if(meta == null) if(meta == null) {
client.newCall(urlToDetailsRequest(url)) client.newCall(urlToDetailsRequest(url))
.asObservableSuccess() .asObservableSuccess()
.map { .map {
rawParseGallery(jsonParser.parse(it.body()!!.string()) realmTrans { realm ->
.asJsonObject) realm.copyFromRealm(realm.createUUIDObj(queryAll().clazz.java).apply {
}.first() metaParser(this,
else jsonParser.parse(it.body()!!.string()).asJsonObject)
})
}
}
.first()
} else {
Observable.just(realm.copyFromRealm(meta)) Observable.just(realm.copyFromRealm(meta))
}
} }
override fun fetchChapterList(manga: SManga) override fun fetchChapterList(manga: SManga)
@ -184,8 +174,7 @@ class NHentai(context: Context) : HttpSource() {
listOf(SChapter.create().apply { listOf(SChapter.create().apply {
url = manga.url url = manga.url
name = "Chapter" name = "Chapter"
//TODO Get this working later date_upload = ((it.uploadDate ?: 0) * 1000)
// date_upload = it.uploadDate ?: 0
chapter_number = 1f chapter_number = 1f
}) })
}!! }!!
@ -241,6 +230,9 @@ class NHentai(context: Context) : HttpSource() {
override val supportsLatest = true override val supportsLatest = true
override fun queryAll() = NHentaiMetadata.EmptyQuery()
override fun queryFromUrl(url: String) = NHentaiMetadata.UrlQuery(url)
companion object { companion object {
val jsonParser by lazy { val jsonParser by lazy {
JsonParser() JsonParser()

View File

@ -3,32 +3,29 @@ package eu.kanade.tachiyomi.source.online.all
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.ChapterRecognition import eu.kanade.tachiyomi.util.ChapterRecognition
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.metadata.copyTo import exh.metadata.models.*
import exh.metadata.loadPervEden
import exh.metadata.models.PervEdenGalleryMetadata
import exh.metadata.models.PervEdenTitle
import exh.metadata.models.Tag
import exh.util.UriFilter import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
import exh.util.createUUIDObj import exh.util.urlImportFetchSearchManga
import exh.util.realmTrans
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode import org.jsoup.nodes.TextNode
import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* 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<PervEdenGalleryMetadata, Document> {
override val supportsLatest = true override val supportsLatest = true
override val name = "Perv Eden" override val name = "Perv Eden"
override val baseUrl = "http://www.perveden.com" override val baseUrl = "http://www.perveden.com"
override val lang = pvLang.name
override fun popularMangaSelector() = "#topManga > ul > li" 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 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 searchMangaSelector() = "#mangaList > tbody > tr"
override fun searchMangaFromElement(element: Element): SManga { 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 { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val uri = Uri.parse("$baseUrl/$lang/$lang-directory/").buildUpon() val uri = Uri.parse("$baseUrl/$lang/$lang-directory/").buildUpon()
uri.appendQueryParameter("page", page.toString()) uri.appendQueryParameter("page", page.toString())
uri.appendQueryParameter("title", query)
filters.forEach { filters.forEach {
if(it is UriFilter) it.addToUri(uri) 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!") throw NotImplementedError("Unused method called!")
} }
override fun mangaDetailsParse(document: Document): SManga { override val metaParser: PervEdenGalleryMetadata.(Document) -> Unit = { document ->
realmTrans { realm -> url = Uri.parse(document.location()).path
val url = document.location()
val metadata = (realm.loadPervEden(PervEdenGalleryMetadata.pvIdFromUrl(url), id)
?: realm.createUUIDObj(PervEdenGalleryMetadata::class.java))
with(metadata) {
this.url = url
lang = this@PervEden.lang 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() val rightBoxElement = document.select(".rightBox:not(.info)").first()
var inStatus: String? = null
rightBoxElement.childNodes().forEach { altTitles.clear()
if(it is Element && it.tagName().toLowerCase() == "h4") { tags.clear()
inStatus = it.text().trim() var inStatus: String? = null
} else { rightBoxElement.childNodes().forEach {
when(inStatus) { if(it is Element && it.tagName().toLowerCase() == "h4") {
"Alternative name(s)" -> { inStatus = it.text().trim()
if(it is TextNode) { } else {
val text = it.text().trim() when(inStatus) {
if(!text.isBlank()) "Alternative name(s)" -> {
altTitles.add(PervEdenTitle(this, text)) if(it is TextNode) {
} val text = it.text().trim()
} if(!text.isBlank())
"Artist" -> { altTitles.add(PervEdenTitle(this, text))
if(it is Element && it.tagName() == "a") { }
artist = it.text() }
tags.add(Tag("artist", it.text().toLowerCase(), false)) "Artist" -> {
} if(it is Element && it.tagName() == "a") {
} artist = it.text()
"Genres" -> { tags.add(Tag("artist", it.text().toLowerCase(), false))
if(it is Element && it.tagName() == "a") }
tags.add(Tag("genre", it.text().toLowerCase(), false)) }
} "Genres" -> {
"Type" -> { if(it is Element && it.tagName() == "a")
if(it is TextNode) { tags.add(Tag("genre", it.text().toLowerCase(), false))
val text = it.text().trim() }
if(!text.isBlank()) "Type" -> {
type = text if(it is TextNode) {
} val text = it.text().trim()
} if(!text.isBlank())
"Status" -> { type = text
if(it is TextNode) { }
val text = it.text().trim() }
if(!text.isBlank()) "Status" -> {
status = text 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 { override fun latestUpdatesRequest(page: Int): Request {
val num = if(lang == "en") "0" val num = when (lang) {
else if(lang == "it") "1" "en" -> "0"
else throw NotImplementedError("Unimplemented language!") "it" -> "1"
else -> throw NotImplementedError("Unimplemented language!")
}
return GET("$baseUrl/ajax/news/$page/$num/0/") 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) override fun imageUrlParse(document: Document)
= "http:" + document.getElementById("mainImg").attr("src")!! = "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 ( override fun getFilterList() = FilterList (
AuthorFilter(), AuthorFilter(),
ArtistFilter(), 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 //Explicit type arg for listOf() to workaround this: KT-16570
class ReleaseYearGroup : UriGroup<Filter<*>>("Release Year", listOf<Filter<*>>( class ReleaseYearGroup : UriGroup<Filter<*>>("Release Year", listOf(
ReleaseYearRangeFilter(), ReleaseYearRangeFilter(),
ReleaseYearYearFilter() ReleaseYearYearFilter()
)) ))

View File

@ -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<HentaiCafeMetadata, Document> {
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<ShowBooksOnlyFilter>().any { it.state }) {
//Filter by book
"$baseUrl/category/book/page/$page/"
} else {
//Filter by tag
val tagFilter = filters.filterIsInstance<TagFilter>().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<List<SChapter>> {
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<Page> {
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<HCTag>("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
}
}

View File

@ -4,9 +4,6 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import exh.* import exh.*
import exh.metadata.metadataClass 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.models.SearchableGalleryMetadata
import exh.metadata.syncMangaIds import exh.metadata.syncMangaIds
import exh.search.SearchEngine import exh.search.SearchEngine
@ -89,7 +86,7 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) :
val meta: RealmResults<out SearchableGalleryMetadata> = if (it.value.isNotEmpty()) val meta: RealmResults<out SearchableGalleryMetadata> = if (it.value.isNotEmpty())
searchEngine.filterResults(it.value.where(), searchEngine.filterResults(it.value.where(),
parsedQuery, parsedQuery,
it.value.first().titleFields) it.value.first()!!.titleFields)
.findAllSorted(SearchableGalleryMetadata::mangaId.name).apply { .findAllSorted(SearchableGalleryMetadata::mangaId.name).apply {
totalFilteredSize += size totalFilteredSize += size
} }
@ -132,7 +129,7 @@ class LibraryCategoryAdapter(val view: LibraryCategoryView) :
} }
} }
} catch (e: Exception) { } 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 //Fallback to regular filter

View File

@ -4,6 +4,7 @@ import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.support.v7.preference.PreferenceScreen import android.support.v7.preference.PreferenceScreen
import android.view.View import android.view.View
import android.widget.Toast
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler 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.base.controller.DialogController
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import exh.ui.migration.MetadataFetchDialog
import exh.util.realmTrans
import io.realm.Realm
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
@ -69,6 +73,38 @@ class SettingsAdvancedController : SettingsController() {
onClick { LibraryUpdateService.start(context, target = Target.TRACKING) } 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() { private fun clearChapterCache() {

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.setting
import android.support.v7.preference.PreferenceScreen import android.support.v7.preference.PreferenceScreen
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import exh.ui.migration.MetadataFetchDialog
import exh.ui.login.LoginController import exh.ui.login.LoginController
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -124,23 +123,5 @@ class SettingsEhController : SettingsController() {
"tr_20" "tr_20"
) )
}.dependency = "enable_exhentai" }.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)
}
}
}
}
} }
} }

View File

@ -48,6 +48,12 @@ class SettingsMainController : SettingsController() {
titleRes = R.string.pref_category_eh titleRes = R.string.pref_category_eh
onClick { navigateTo(SettingsEhController()) } onClick { navigateTo(SettingsEhController()) }
} }
preference {
iconRes = R.drawable.eh_ic_nhlogo_color
iconTint = tintColor
titleRes = R.string.pref_category_nh
onClick { navigateTo(SettingsNhController()) }
}
preference { preference {
iconRes = R.drawable.ic_code_black_24dp iconRes = R.drawable.ic_code_black_24dp
iconTint = tintColor iconTint = tintColor

View File

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

View File

@ -15,6 +15,8 @@ val PERV_EDEN_IT_SOURCE_ID = LEWD_SOURCE_SERIES + 6
val NHENTAI_SOURCE_ID = LEWD_SOURCE_SERIES + 7 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 isLewdSource(source: Long) = source in 6900..6999
fun isEhSource(source: Long) = source == EH_SOURCE_ID fun isEhSource(source: Long) = source == EH_SOURCE_ID

View File

@ -10,19 +10,16 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource 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.ExGalleryMetadata
import exh.metadata.models.NHentaiMetadata import exh.metadata.models.NHentaiMetadata
import exh.metadata.models.PervEdenGalleryMetadata
import exh.metadata.models.PervEdenLang
import exh.util.defRealm import exh.util.defRealm
import io.realm.Realm
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.MalformedURLException
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
@ -70,10 +67,19 @@ class GalleryAdder {
forceSource: Long? = null): GalleryAddEvent { forceSource: Long? = null): GalleryAddEvent {
try { try {
val urlObj = Uri.parse(url) 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 "g.e-hentai.org", "e-hentai.org" -> EH_SOURCE_ID
"exhentai.org" -> EXH_SOURCE_ID "exhentai.org" -> EXH_SOURCE_ID
"nhentai.net" -> NHENTAI_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) else -> return GalleryAddEvent.Fail.UnknownType(url)
} }
@ -81,7 +87,6 @@ class GalleryAdder {
return GalleryAddEvent.Fail.UnknownType(url) return GalleryAddEvent.Fail.UnknownType(url)
} }
val firstPathSegment = urlObj.pathSegments.firstOrNull()?.toLowerCase()
val realUrl = when(source) { val realUrl = when(source) {
EH_SOURCE_ID, EXH_SOURCE_ID -> when (firstPathSegment) { EH_SOURCE_ID, EXH_SOURCE_ID -> when (firstPathSegment) {
"g" -> { "g" -> {
@ -94,10 +99,19 @@ class GalleryAdder {
} }
else -> return GalleryAddEvent.Fail.UnknownType(url) else -> return GalleryAddEvent.Fail.UnknownType(url)
} }
NHENTAI_SOURCE_ID -> when { NHENTAI_SOURCE_ID -> {
firstPathSegment == "g" -> url if(firstPathSegment != "g")
urlObj.pathSegments.size >= 3 -> "https://nhentai.net/g/${urlObj.pathSegments[1]}/" return GalleryAddEvent.Fail.UnknownType(url)
else -> 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) else -> return GalleryAddEvent.Fail.UnknownType(url)
} }
@ -108,6 +122,8 @@ class GalleryAdder {
val cleanedUrl = when(source) { val cleanedUrl = when(source) {
EH_SOURCE_ID, EXH_SOURCE_ID -> getUrlWithoutDomain(realUrl) EH_SOURCE_ID, EXH_SOURCE_ID -> getUrlWithoutDomain(realUrl)
NHENTAI_SOURCE_ID -> realUrl //nhentai uses URLs directly (oops, my bad when implementing this source) 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) else -> return GalleryAddEvent.Fail.UnknownType(url)
} }
@ -119,17 +135,27 @@ class GalleryAdder {
} }
//Copy basics //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 //Apply metadata
defRealm { realm -> defRealm { realm ->
when (source) { when (source) {
EH_SOURCE_ID, EXH_SOURCE_ID -> EH_SOURCE_ID, EXH_SOURCE_ID ->
realm.loadEh(ExGalleryMetadata.galleryId(realUrl), ExGalleryMetadata.UrlQuery(realUrl, isExSource(source))
ExGalleryMetadata.galleryToken(realUrl), .query(realm)
isExSource(source))?.copyTo(manga) .findFirst()?.copyTo(manga)
NHENTAI_SOURCE_ID -> 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) ?.copyTo(manga)
else -> return GalleryAddEvent.Fail.UnknownType(url) else -> return GalleryAddEvent.Fail.UnknownType(url)
} }
@ -160,16 +186,16 @@ class GalleryAdder {
} }
private fun getUrlWithoutDomain(orig: String): String { private fun getUrlWithoutDomain(orig: String): String {
try { return try {
val uri = URI(orig) val uri = URI(orig)
var out = uri.path var out = uri.path
if (uri.query != null) if (uri.query != null)
out += "?" + uri.query out += "?" + uri.query
if (uri.fragment != null) if (uri.fragment != null)
out += "#" + uri.fragment out += "#" + uri.fragment
return out out
} catch (e: URISyntaxException) { } catch (e: URISyntaxException) {
return orig orig
} }
} }
} }

View File

@ -1,121 +1,32 @@
package exh.metadata package exh.metadata
import eu.kanade.tachiyomi.data.database.models.Manga 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 eu.kanade.tachiyomi.ui.library.LibraryItem
import exh.* import exh.*
import exh.metadata.models.ExGalleryMetadata import exh.metadata.models.*
import exh.metadata.models.NHentaiMetadata
import exh.metadata.models.PervEdenGalleryMetadata
import exh.metadata.models.SearchableGalleryMetadata
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import rx.Observable
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.reflect.KClass import kotlin.reflect.KClass
fun Realm.ehMetaQueryFromUrl(url: String,
exh: Boolean,
meta: RealmQuery<ExGalleryMetadata>? = null) =
ehMetadataQuery(
ExGalleryMetadata.galleryId(url),
ExGalleryMetadata.galleryToken(url),
exh,
meta
)
fun Realm.ehMetadataQuery(gId: String,
gToken: String,
exh: Boolean,
meta: RealmQuery<ExGalleryMetadata>? = null)
= (meta ?: where(ExGalleryMetadata::class.java))
.equalTo(ExGalleryMetadata::gId.name, gId)
.equalTo(ExGalleryMetadata::gToken.name, gToken)
.equalTo(ExGalleryMetadata::exh.name, exh)
fun Realm.loadEh(gId: String, gToken: String, exh: Boolean): ExGalleryMetadata?
= ehMetadataQuery(gId, gToken, exh)
.findFirst()
fun Realm.loadEhAsync(gId: String, gToken: String, exh: Boolean): Observable<ExGalleryMetadata?>
= ehMetadataQuery(gId, gToken, exh)
.findFirstAsync()
.asObservable()
private fun pervEdenSourceToLang(source: Long)
= when (source) {
PERV_EDEN_EN_SOURCE_ID -> "en"
PERV_EDEN_IT_SOURCE_ID -> "it"
else -> throw IllegalArgumentException()
}
fun Realm.pervEdenMetaQueryFromUrl(url: String,
source: Long,
meta: RealmQuery<PervEdenGalleryMetadata>? = null) =
pervEdenMetadataQuery(
PervEdenGalleryMetadata.pvIdFromUrl(url),
source,
meta
)
fun Realm.pervEdenMetadataQuery(pvId: String,
source: Long,
meta: RealmQuery<PervEdenGalleryMetadata>? = null)
= (meta ?: where(PervEdenGalleryMetadata::class.java))
.equalTo(PervEdenGalleryMetadata::lang.name, pervEdenSourceToLang(source))
.equalTo(PervEdenGalleryMetadata::pvId.name, pvId)
fun Realm.loadPervEden(pvId: String, source: Long): PervEdenGalleryMetadata?
= pervEdenMetadataQuery(pvId, source)
.findFirst()
fun Realm.loadPervEdenAsync(pvId: String, source: Long): Observable<PervEdenGalleryMetadata?>
= pervEdenMetadataQuery(pvId, source)
.findFirstAsync()
.asObservable()
fun Realm.nhentaiMetaQueryFromUrl(url: String,
meta: RealmQuery<NHentaiMetadata>? = null) =
nhentaiMetadataQuery(
NHentaiMetadata.nhIdFromUrl(url),
meta
)
fun Realm.nhentaiMetadataQuery(nhId: Long,
meta: RealmQuery<NHentaiMetadata>? = null)
= (meta ?: where(NHentaiMetadata::class.java))
.equalTo(NHentaiMetadata::nhId.name, nhId)
fun Realm.loadNhentai(nhId: Long): NHentaiMetadata?
= nhentaiMetadataQuery(nhId)
.findFirst()
fun Realm.loadNhentaiAsync(nhId: Long): Observable<NHentaiMetadata?>
= nhentaiMetadataQuery(nhId)
.findFirstAsync()
.asObservable()
fun Realm.loadAllMetadata(): Map<KClass<out SearchableGalleryMetadata>, RealmResults<out SearchableGalleryMetadata>> = fun Realm.loadAllMetadata(): Map<KClass<out SearchableGalleryMetadata>, RealmResults<out SearchableGalleryMetadata>> =
listOf<Pair<KClass<out SearchableGalleryMetadata>, RealmQuery<out SearchableGalleryMetadata>>>( Injekt.get<SourceManager>().getOnlineSources().filterIsInstance<LewdSource<*, *>>().map {
Pair(ExGalleryMetadata::class, where(ExGalleryMetadata::class.java)), it.queryAll()
Pair(NHentaiMetadata::class, where(NHentaiMetadata::class.java)), }.associate {
Pair(PervEdenGalleryMetadata::class, where(PervEdenGalleryMetadata::class.java)) it.clazz to it.query(this@loadAllMetadata).findAllSorted(SearchableGalleryMetadata::mangaId.name)
).map {
Pair(it.first, it.second.findAllSorted(SearchableGalleryMetadata::mangaId.name))
}.toMap() }.toMap()
fun Realm.queryMetadataFromManga(manga: Manga, fun Realm.queryMetadataFromManga(manga: Manga,
meta: RealmQuery<out SearchableGalleryMetadata>? = null): RealmQuery<out SearchableGalleryMetadata> = meta: RealmQuery<SearchableGalleryMetadata>? = null):
when(manga.source) { RealmQuery<out SearchableGalleryMetadata> =
EH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, false, meta as? RealmQuery<ExGalleryMetadata>) Injekt.get<SourceManager>().get(manga.source)?.let {
EXH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, true, meta as? RealmQuery<ExGalleryMetadata>) (it as LewdSource<*, *>).queryFromUrl(manga.url) as GalleryQuery<SearchableGalleryMetadata>
PERV_EDEN_EN_SOURCE_ID, }?.query(this, meta) ?: throw IllegalArgumentException("Unknown source type!")
PERV_EDEN_IT_SOURCE_ID ->
pervEdenMetaQueryFromUrl(manga.url, manga.source, meta as? RealmQuery<PervEdenGalleryMetadata>)
NHENTAI_SOURCE_ID -> nhentaiMetaQueryFromUrl(manga.url, meta as? RealmQuery<NHentaiMetadata>)
else -> throw IllegalArgumentException("Unknown source type!")
}
fun Realm.syncMangaIds(mangas: List<LibraryItem>) { fun Realm.syncMangaIds(mangas: List<LibraryItem>) {
Timber.d("--> EH: Begin syncing ${mangas.size} manga IDs...") Timber.d("--> EH: Begin syncing ${mangas.size} manga IDs...")
@ -138,11 +49,4 @@ fun Realm.syncMangaIds(mangas: List<LibraryItem>) {
} }
val Manga.metadataClass val Manga.metadataClass
get() = when (source) { get() = (Injekt.get<SourceManager>().get(source) as? LewdSource<*, *>)?.queryAll()?.clazz
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
}

View File

@ -1,5 +1,10 @@
package exh.metadata package exh.metadata
import exh.metadata.models.SearchableGalleryMetadata
import exh.plusAssign
import java.text.SimpleDateFormat
import java.util.*
/** /**
* Metadata utils * Metadata utils
*/ */
@ -44,4 +49,37 @@ fun <T> ignore(expr: () -> T): T? {
fun <K,V> Set<Map.Entry<K,V>>.forEach(action: (K, V) -> Unit) { fun <K,V> Set<Map.Entry<K,V>>.forEach(action: (K, V) -> Unit) {
forEach { action(it.key, it.value) } forEach { action(it.key, it.value) }
} }
val ONGOING_SUFFIX = arrayOf(
"[ongoing]",
"(ongoing)",
"{ongoing}",
"<ongoing>",
"ongoing",
"[incomplete]",
"(incomplete)",
"{incomplete}",
"<incomplete>",
"incomplete",
"[wip]",
"(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"
}
}
}

View File

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

View File

@ -1,12 +1,22 @@
package exh.metadata.models package exh.metadata.models
import android.net.Uri 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.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.Index import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass import io.realm.annotations.RealmClass
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.* import java.util.*
/** /**
@ -61,12 +71,99 @@ open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata {
@Index @Index
override var mangaId: Long? = null override var mangaId: Long? = null
class EmptyQuery : GalleryQuery<ExGalleryMetadata>(ExGalleryMetadata::class)
class UrlQuery(
val url: String,
val exh: Boolean
) : GalleryQuery<ExGalleryMetadata>(ExGalleryMetadata::class) {
override fun transform() = Query(
galleryId(url),
galleryToken(url),
exh
)
}
class Query(val gId: String,
val gToken: String,
val exh: Boolean
) : GalleryQuery<ExGalleryMetadata>(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<PreferencesHelper>().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 { companion object {
private fun splitGalleryUrl(url: String) private fun splitGalleryUrl(url: String)
= url.let { = url.let {
Uri.parse(it).pathSegments Uri.parse(it).pathSegments
.filterNot(String::isNullOrBlank) .filterNot(String::isNullOrBlank)
} }
fun galleryId(url: String) = splitGalleryUrl(url).let { it[it.size - 2] } fun galleryId(url: String) = splitGalleryUrl(url).let { it[it.size - 2] }
@ -77,5 +174,9 @@ open class ExGalleryMetadata : RealmObject(), SearchableGalleryMetadata {
ExGalleryMetadata::title.name, ExGalleryMetadata::title.name,
ExGalleryMetadata::altTitle.name ExGalleryMetadata::altTitle.name
) )
private const val EH_ARTIST_NAMESPACE = "artist"
private const val EH_AUTHOR_NAMESPACE = "author"
} }
} }

View File

@ -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<T : SearchableGalleryMetadata>(val clazz: KClass<T>) {
open fun map(): Map<*, *> = emptyMap<KProperty<T>, KProperty1<GalleryQuery<T>, *>>()
open fun transform(): GalleryQuery<T>? = this
open fun override(meta: RealmQuery<T>): RealmQuery<T> = meta
fun query(realm: Realm, meta: RealmQuery<T>? = null): RealmQuery<T>
= (meta ?: realm.where(clazz.java)).let {
val visited = mutableListOf<GalleryQuery<T>>()
var top: GalleryQuery<T>? = 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<T>): RealmQuery<T> {
var newMeta = meta
map().forEach { (t, u) ->
t as KProperty<T>
u as KProperty1<GalleryQuery<T>, *>
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
}
}

View File

@ -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<Tag> = 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>(HentaiCafeMetadata::class)
class UrlQuery(
val url: String
) : GalleryQuery<HentaiCafeMetadata>(HentaiCafeMetadata::class) {
override fun transform() = Query(
hcIdFromUrl(url)
)
}
class Query(val hcId: String): GalleryQuery<HentaiCafeMetadata>(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() }
}
}

View File

@ -1,11 +1,21 @@
package exh.metadata.models 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.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.Index import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass import io.realm.annotations.RealmClass
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.* import java.util.*
/** /**
@ -58,18 +68,92 @@ open class NHentaiMetadata : RealmObject(), SearchableGalleryMetadata {
@Index @Index
override var mangaId: Long? = null override var mangaId: Long? = null
class EmptyQuery : GalleryQuery<NHentaiMetadata>(NHentaiMetadata::class)
class UrlQuery(
val url: String
) : GalleryQuery<NHentaiMetadata>(NHentaiMetadata::class) {
override fun transform() = Query(
nhIdFromUrl(url)
)
}
class Query(
val nhId: Long
) : GalleryQuery<NHentaiMetadata>(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<PreferencesHelper>().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 { companion object {
val BASE_URL = "https://nhentai.net" val BASE_URL = "https://nhentai.net"
private const val NHENTAI_ARTIST_NAMESPACE = "artist"
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
fun typeToExtension(t: String?) = fun typeToExtension(t: String?) =
when(t) { when(t) {
"p" -> "png" "p" -> "png"
"j" -> "jpg" "j" -> "jpg"
else -> null else -> null
} }
fun nhIdFromUrl(url: String) fun nhIdFromUrl(url: String)
= url.split("/").last { it.isNotBlank() }.toLong() = url.split("/").last { it.isNotBlank() }.toLong()
val TITLE_FIELDS = listOf( val TITLE_FIELDS = listOf(
NHentaiMetadata::japaneseTitle.name, NHentaiMetadata::japaneseTitle.name,

View File

@ -1,8 +1,14 @@
package exh.metadata.models package exh.metadata.models
import android.net.Uri 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.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmQuery
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.Index import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
@ -50,11 +56,79 @@ open class PervEdenGalleryMetadata : RealmObject(), SearchableGalleryMetadata {
@Index @Index
override var mangaId: Long? = null 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>(PervEdenGalleryMetadata::class)
class UrlQuery(
val url: String,
val lang: PervEdenLang
) : GalleryQuery<PervEdenGalleryMetadata>(PervEdenGalleryMetadata::class) {
override fun transform() = Query(
pvIdFromUrl(url),
lang
)
}
class Query(val pvId: String,
val lang: PervEdenLang
) : GalleryQuery<PervEdenGalleryMetadata>(PervEdenGalleryMetadata::class) {
override fun map() = mapOf(
PervEdenGalleryMetadata::pvId to Query::pvId
)
override fun override(meta: RealmQuery<PervEdenGalleryMetadata>)
= meta.equalTo(PervEdenGalleryMetadata::lang.name, lang.name)
}
companion object { companion object {
private fun splitGalleryUrl(url: String) private fun splitGalleryUrl(url: String)
= url.let { = url.let {
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank) Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
} }
fun pvIdFromUrl(url: String) = splitGalleryUrl(url).last() 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)" 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!")
}
}

View File

@ -1,11 +1,8 @@
package exh.metadata.models package exh.metadata.models
import eu.kanade.tachiyomi.source.model.SManga
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmModel 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 * A gallery that can be searched using the EH search engine
@ -23,4 +20,6 @@ interface SearchableGalleryMetadata: RealmModel {
val titleFields: List<String> val titleFields: List<String>
var mangaId: Long? var mangaId: Long?
fun copyTo(manga: SManga)
} }

View File

@ -18,12 +18,11 @@ class SearchEngine {
fun matchTagList(namespace: String?, fun matchTagList(namespace: String?,
component: Text?, component: Text?,
excluded: Boolean) { excluded: Boolean) {
if(excluded) when {
rQuery.not() excluded -> rQuery.not()
else if (queryEmpty) queryEmpty -> queryEmpty = false
queryEmpty = false else -> rQuery.or()
else }
rQuery.or()
rQuery.beginGroup() rQuery.beginGroup()
//Match namespace if specified //Match namespace if specified

View File

@ -11,10 +11,8 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import exh.isExSource import exh.isExSource
import exh.isLewdSource import exh.isLewdSource
import exh.metadata.genericCopyTo
import exh.metadata.queryMetadataFromManga import exh.metadata.queryMetadataFromManga
import exh.util.defRealm import exh.util.defRealm
import exh.util.realmTrans
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -64,7 +62,7 @@ class MetadataFetchDialog {
val source = sourceManager.get(manga.source) val source = sourceManager.get(manga.source)
source?.let { source?.let {
manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first()) manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first())
realm.queryMetadataFromManga(manga).findFirst()?.genericCopyTo(manga) realm.queryMetadataFromManga(manga).findFirst()?.copyTo(manga)
} }
} catch (t: Throwable) { } catch (t: Throwable) {
Timber.e(t, "Could not migrate manga!") Timber.e(t, "Could not migrate manga!")

View File

@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import exh.isExSource import exh.isExSource
import exh.isLewdSource import exh.isLewdSource
import exh.metadata.ehMetaQueryFromUrl import exh.metadata.models.ExGalleryMetadata
import exh.util.realmTrans import exh.util.realmTrans
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -43,7 +43,9 @@ class UrlMigrator {
//Build fixed URL //Build fixed URL
val urlWithSlash = "/" + manga.url val urlWithSlash = "/" + manga.url
//Fix metadata if required //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 { metadata?.url?.let {
if (it.startsWith("g/")) { //Check if metadata URL has no slash if (it.startsWith("g/")) { //Check if metadata URL has no slash
metadata.url = urlWithSlash //Fix it metadata.url = urlWithSlash //Fix it

View File

@ -480,19 +480,19 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
return query.average(fieldName) return query.average(fieldName)
} }
fun min(fieldName: String): Number { fun min(fieldName: String): Number? {
return query.min(fieldName) return query.min(fieldName)
} }
fun minimumDate(fieldName: String): Date { fun minimumDate(fieldName: String): Date? {
return query.minimumDate(fieldName) return query.minimumDate(fieldName)
} }
fun max(fieldName: String): Number { fun max(fieldName: String): Number? {
return query.max(fieldName) return query.max(fieldName)
} }
fun maximumDate(fieldName: String): Date { fun maximumDate(fieldName: String): Date? {
return query.maximumDate(fieldName) return query.maximumDate(fieldName)
} }
@ -540,7 +540,7 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
return query.findAllSortedAsync(fieldName1, sortOrder1, fieldName2, sortOrder2) return query.findAllSortedAsync(fieldName1, sortOrder1, fieldName2, sortOrder2)
} }
fun findFirst(): E { fun findFirst(): E? {
return query.findFirst() return query.findFirst()
} }

View File

@ -7,24 +7,8 @@ import java.util.*
inline fun <T> realmTrans(block: (Realm) -> T): T { inline fun <T> realmTrans(block: (Realm) -> T): T {
return defRealm { return defRealm {
it.beginTransaction() it.trans {
try { block(it)
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()
}
} }
} }
} }
@ -35,5 +19,27 @@ inline fun <T> defRealm(block: (Realm) -> T): T {
} }
} }
inline fun <T> 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 <T : RealmModel> Realm.createUUIDObj(clazz: Class<T>) fun <T : RealmModel> Realm.createUUIDObj(clazz: Class<T>)
= createObject(clazz, UUID.randomUUID().toString()) = createObject(clazz, UUID.randomUUID().toString())!!

View File

@ -0,0 +1,15 @@
<vector android:height="24dp" android:viewportHeight="470.25"
android:viewportWidth="603.195" android:width="30dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group
android:translateY="100"
android:scaleX="1.2"
android:scaleY="1.2">
<path android:fillColor="#EC2854"
android:pathData="M172.2,36.03c-16.6,6.91 -52.73,34.03 -36.25,58.47c7.29,10.81 19.94,18.44 31.47,22.06c10.73,3.36 23.9,-0.76 33.71,3.72c-2.09,5.1 -9.48,23.69 -15.81,22.32c-11.83,-2.54 -23.79,-0.44 -33.07,8.48c-18.96,-26.3 -45.97,-36.97 -75.74,-29.68c22.07,-27.2 16.72,-55.69 -6.47,-81.62c-14,-15.66 -47.99,-37.96 -69.85,-28.85C54.78,-11.81 121.31,5.38 172.2,36.03C163.38,39.7 168.58,33.85 172.2,36.03z"
android:strokeColor="#EC2854" android:strokeWidth="1"/>
<path android:fillColor="#EC2854"
android:pathData="M310.36,36.03c16.59,6.91 52.73,34.03 36.25,58.47c-7.29,10.81 -19.94,18.44 -31.47,22.06c-10.73,3.37 -23.9,-0.76 -33.71,3.72c2.1,5.11 9.46,23.67 15.81,22.32c11.83,-2.54 23.79,-0.45 33.07,8.48c18.96,-26.29 45.97,-36.97 75.74,-29.68c-22.06,-27.21 -16.73,-55.68 6.47,-81.62c14,-15.65 47.99,-37.97 69.85,-28.85C427.78,-11.8 361.25,5.37 310.36,36.03C319.18,39.7 313.98,33.85 310.36,36.03z"
android:strokeColor="#EC2854" android:strokeWidth="1"/>
<path android:fillColor="#FFFFFF" android:pathData="M180.43,97.11h23.2v15.92c6.87,-6.56 14.15,-11.27 21.84,-14.14c7.69,-2.86 16.23,-4.3 25.64,-4.3c20.62,0 34.55,5.55 41.78,16.65c3.98,6.07 5.97,14.77 5.97,26.08v71.95h-24.83v-70.69c0,-6.84 -1.31,-12.36 -3.93,-16.55c-4.34,-6.98 -12.21,-10.47 -23.6,-10.47c-5.79,0 -10.54,0.46 -14.25,1.36c-6.69,1.54 -12.57,4.61 -17.64,9.22c-4.07,3.7 -6.72,7.52 -7.94,11.47c-1.22,3.94 -1.83,9.58 -1.83,16.91v58.75h-24.42L180.43,97.11L180.43,97.11z"/>
</group>
</vector>

View File

@ -451,4 +451,5 @@
<!-- EXH --> <!-- EXH -->
<string name="label_login">Login</string> <string name="label_login">Login</string>
<string name="pref_category_eh">E-Hentai</string> <string name="pref_category_eh">E-Hentai</string>
<string name="pref_category_nh">nhentai</string>
</resources> </resources>