mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Various changes
This commit is contained in:
		| @@ -117,4 +117,5 @@ object PreferenceKeys { | ||||
|  | ||||
|     fun trackToken(syncId: Int) = "track_token_$syncId" | ||||
|  | ||||
|     const val eh_nh_useHighQualityThumbs = "eh_nh_hq_thumbs" | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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() | ||||
|     )) | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										21
									
								
								app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsNhController.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										21
									
								
								app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsNhController.kt
									
									
									
									
									
										Executable 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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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" | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -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" | ||||
|  | ||||
|     } | ||||
| } | ||||
							
								
								
									
										68
									
								
								app/src/main/java/exh/metadata/models/GalleryQuery.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										68
									
								
								app/src/main/java/exh/metadata/models/GalleryQuery.kt
									
									
									
									
									
										Executable 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 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										91
									
								
								app/src/main/java/exh/metadata/models/HentaiCafeMetadata.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								app/src/main/java/exh/metadata/models/HentaiCafeMetadata.kt
									
									
									
									
									
										Normal 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() } | ||||
|     } | ||||
| } | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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!") | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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!") | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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())!! | ||||
|   | ||||
		Reference in New Issue
	
	Block a user