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
*/build
/mainframer.sh
/mainframer
/.mainframer
*.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"
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 {

View File

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

View File

@ -117,4 +117,5 @@ object PreferenceKeys {
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 lockUseFingerprint() = rxPrefs.getBoolean("lock_finger", false)
fun eh_useHighQualityThumbs() = rxPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false)
// <-- 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.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
}

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.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<ExGalleryMetadata, Response> {
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<String, String>)
= 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"

View File

@ -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<NHentaiMetadata, JsonObject> {
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
//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<SManga> {
@ -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()

View File

@ -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<PervEdenGalleryMetadata, Document> {
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<Filter<*>>("Release Year", listOf<Filter<*>>(
class ReleaseYearGroup : UriGroup<Filter<*>>("Release Year", listOf(
ReleaseYearRangeFilter(),
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 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<out SearchableGalleryMetadata> = 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

View File

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

View File

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

View File

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

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 HENTAI_CAFE_SOURCE_ID = LEWD_SOURCE_SERIES + 8
fun isLewdSource(source: Long) = source in 6900..6999
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.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
}
}
}

View File

@ -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<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>> =
listOf<Pair<KClass<out SearchableGalleryMetadata>, RealmQuery<out SearchableGalleryMetadata>>>(
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<SourceManager>().getOnlineSources().filterIsInstance<LewdSource<*, *>>().map {
it.queryAll()
}.associate {
it.clazz to it.query(this@loadAllMetadata).findAllSorted(SearchableGalleryMetadata::mangaId.name)
}.toMap()
fun Realm.queryMetadataFromManga(manga: Manga,
meta: RealmQuery<out SearchableGalleryMetadata>? = null): RealmQuery<out SearchableGalleryMetadata> =
when(manga.source) {
EH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, false, meta as? RealmQuery<ExGalleryMetadata>)
EXH_SOURCE_ID -> ehMetaQueryFromUrl(manga.url, true, meta as? RealmQuery<ExGalleryMetadata>)
PERV_EDEN_EN_SOURCE_ID,
PERV_EDEN_IT_SOURCE_ID ->
pervEdenMetaQueryFromUrl(manga.url, manga.source, meta as? RealmQuery<PervEdenGalleryMetadata>)
NHENTAI_SOURCE_ID -> nhentaiMetaQueryFromUrl(manga.url, meta as? RealmQuery<NHentaiMetadata>)
else -> throw IllegalArgumentException("Unknown source type!")
}
meta: RealmQuery<SearchableGalleryMetadata>? = null):
RealmQuery<out SearchableGalleryMetadata> =
Injekt.get<SourceManager>().get(manga.source)?.let {
(it as LewdSource<*, *>).queryFromUrl(manga.url) as GalleryQuery<SearchableGalleryMetadata>
}?.query(this, meta) ?: throw IllegalArgumentException("Unknown source type!")
fun Realm.syncMangaIds(mangas: List<LibraryItem>) {
Timber.d("--> EH: Begin syncing ${mangas.size} manga IDs...")
@ -138,11 +49,4 @@ fun Realm.syncMangaIds(mangas: List<LibraryItem>) {
}
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<SourceManager>().get(source) as? LewdSource<*, *>)?.queryAll()?.clazz

View File

@ -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 <T> ignore(expr: () -> T): T? {
fun <K,V> Set<Map.Entry<K,V>>.forEach(action: (K, V) -> Unit) {
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
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>(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 {
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"
}
}

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
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>(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 {
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,

View File

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

View File

@ -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<String>
var mangaId: Long?
fun copyTo(manga: SManga)
}

View File

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

View File

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

View File

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

View File

@ -480,19 +480,19 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
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<E : RealmModel>(val query: RealmQuery<E>) {
return query.findAllSortedAsync(fieldName1, sortOrder1, fieldName2, sortOrder2)
}
fun findFirst(): E {
fun findFirst(): E? {
return query.findFirst()
}

View File

@ -7,24 +7,8 @@ import java.util.*
inline fun <T> realmTrans(block: (Realm) -> T): T {
return defRealm {
it.beginTransaction()
try {
val res = block(it)
it.commitTransaction()
res
} catch(t: Throwable) {
if (it.isInTransaction) {
it.cancelTransaction()
} else {
RealmLog.warn("Could not cancel transaction, not currently in a transaction.")
}
throw t
} finally {
//Just in case
if (it.isInTransaction) {
it.cancelTransaction()
}
it.trans {
block(it)
}
}
}
@ -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>)
= 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 -->
<string name="label_login">Login</string>
<string name="pref_category_eh">E-Hentai</string>
<string name="pref_category_nh">nhentai</string>
</resources>