Optimize imports, disallow wildcard imports because of klint, run linter
This commit is contained in:
parent
f18891a07e
commit
23ac3d18e5
@ -43,7 +43,6 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|||||||
put(COL_FLAGS, obj.flags)
|
put(COL_FLAGS, obj.flags)
|
||||||
val orderString = obj.mangaOrder.joinToString("/")
|
val orderString = obj.mangaOrder.joinToString("/")
|
||||||
put(COL_MANGA_ORDER, orderString)
|
put(COL_MANGA_ORDER, orderString)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,4 @@ class MangaUrlPutResolver : PutResolver<Manga>() {
|
|||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
put(MangaTable.COL_URL, manga.url)
|
put(MangaTable.COL_URL, manga.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ object CategoryTable {
|
|||||||
$COL_MANGA_ORDER TEXT NOT NULL
|
$COL_MANGA_ORDER TEXT NOT NULL
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
|
|
||||||
val addMangaOrder: String
|
val addMangaOrder: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT"
|
||||||
}
|
}
|
||||||
|
@ -72,4 +72,4 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
|
||||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,4 +45,4 @@ class EmptyPreferenceDataStore : PreferenceDataStore() {
|
|||||||
|
|
||||||
override fun putStringSet(key: String?, values: Set<String>?) {
|
override fun putStringSet(key: String?, values: Set<String>?) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,4 +57,4 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
|
|||||||
|
|
||||||
return newBody.toString().toRequestBody(requestBody.contentType())
|
return newBody.toString().toRequestBody(requestBody.contentType())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,4 +110,4 @@ internal class UpdaterNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
notificationBuilder.show(Notifications.ID_UPDATER)
|
notificationBuilder.show(Notifications.ID_UPDATER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,4 @@ import androidx.preference.PreferenceScreen
|
|||||||
interface ConfigurableSource : Source {
|
interface ConfigurableSource : Source {
|
||||||
|
|
||||||
fun setupPreferenceScreen(screen: PreferenceScreen)
|
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,11 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
import exh.metadata.metadata.base.insertFlatMetadata
|
import exh.metadata.metadata.base.insertFlatMetadata
|
||||||
|
import kotlin.reflect.KClass
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LEWD!
|
* LEWD!
|
||||||
|
@ -28,4 +28,4 @@ interface UrlImportableSource : Source {
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.source.online.all
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.array
|
||||||
|
import com.github.salomonbrys.kotson.get
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
import com.github.salomonbrys.kotson.obj
|
||||||
|
import com.github.salomonbrys.kotson.set
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
@ -13,7 +18,12 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.asObservableWithAsyncStacktrace
|
import eu.kanade.tachiyomi.network.asObservableWithAsyncStacktrace
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
@ -36,23 +46,30 @@ import exh.util.UriFilter
|
|||||||
import exh.util.UriGroup
|
import exh.util.UriGroup
|
||||||
import exh.util.ignore
|
import exh.util.ignore
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.util.ArrayList
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.*
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
// TODO Consider gallery updating when doing tabbed browsing
|
// TODO Consider gallery updating when doing tabbed browsing
|
||||||
class EHentai(override val id: Long,
|
class EHentai(
|
||||||
val exh: Boolean,
|
override val id: Long,
|
||||||
val context: Context) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
|
val exh: Boolean,
|
||||||
|
val context: Context
|
||||||
|
) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
|
||||||
override val metaClass = EHentaiSearchMetadata::class
|
override val metaClass = EHentaiSearchMetadata::class
|
||||||
|
|
||||||
val schema: String
|
val schema: String
|
||||||
@ -98,10 +115,10 @@ class EHentai(override val id: Long,
|
|||||||
favElement?.attr("style")?.substring(14, 17)
|
favElement?.attr("style")?.substring(14, 17)
|
||||||
),
|
),
|
||||||
manga = Manga.create(id).apply {
|
manga = Manga.create(id).apply {
|
||||||
//Get title
|
// Get title
|
||||||
title = thumbnailElement.attr("title")
|
title = thumbnailElement.attr("title")
|
||||||
url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href"))
|
url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href"))
|
||||||
//Get image
|
// Get image
|
||||||
thumbnail_url = thumbnailElement.attr("src")
|
thumbnail_url = thumbnailElement.attr("src")
|
||||||
|
|
||||||
// TODO Parse genre + uploader + tags
|
// TODO Parse genre + uploader + tags
|
||||||
@ -110,9 +127,9 @@ class EHentai(override val id: Long,
|
|||||||
|
|
||||||
val parsedLocation = doc.location().toHttpUrlOrNull()
|
val parsedLocation = doc.location().toHttpUrlOrNull()
|
||||||
|
|
||||||
//Add to page if required
|
// Add to page if required
|
||||||
val hasNextPage = if (parsedLocation == null
|
val hasNextPage = if (parsedLocation == null ||
|
||||||
|| !parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
|
!parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
|
||||||
select("a[onclick=return false]").last()?.let {
|
select("a[onclick=return false]").last()?.let {
|
||||||
it.text() == ">"
|
it.text() == ">"
|
||||||
} ?: false
|
} ?: false
|
||||||
@ -212,8 +229,11 @@ class EHentai(override val id: Long,
|
|||||||
}
|
}
|
||||||
}!!
|
}!!
|
||||||
|
|
||||||
private fun fetchChapterPage(chapter: SChapter, np: String,
|
private fun fetchChapterPage(
|
||||||
pastUrls: List<String> = emptyList()): Observable<List<String>> {
|
chapter: SChapter,
|
||||||
|
np: String,
|
||||||
|
pastUrls: List<String> = emptyList()
|
||||||
|
): Observable<List<String>> {
|
||||||
val urls = ArrayList(pastUrls)
|
val urls = ArrayList(pastUrls)
|
||||||
return chapterPageCall(np).flatMap {
|
return chapterPageCall(np).flatMap {
|
||||||
val jsoup = it.asJsoup()
|
val jsoup = it.asJsoup()
|
||||||
@ -245,7 +265,7 @@ class EHentai(override val id: Long,
|
|||||||
else
|
else
|
||||||
exGet("$baseUrl/toplist.php?tl=15&p=${page - 1}", null) // Custom page logic for toplists
|
exGet("$baseUrl/toplist.php?tl=15&p=${page - 1}", null) // Custom page logic for toplists
|
||||||
|
|
||||||
//Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(query) {
|
urlImportFetchSearchManga(query) {
|
||||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||||
@ -377,7 +397,7 @@ class EHentai(override val id: Long,
|
|||||||
|
|
||||||
uploader = select("#gdn").text().nullIfBlank()?.trim()
|
uploader = select("#gdn").text().nullIfBlank()?.trim()
|
||||||
|
|
||||||
//Parse the table
|
// Parse the table
|
||||||
select("#gdd tr").forEach {
|
select("#gdd tr").forEach {
|
||||||
val left = it.select(".gdt1").text().nullIfBlank()?.trim()
|
val left = it.select(".gdt1").text().nullIfBlank()?.trim()
|
||||||
val rightElement = it.selectFirst(".gdt2")
|
val rightElement = it.selectFirst(".gdt2")
|
||||||
@ -407,13 +427,13 @@ class EHentai(override val id: Long,
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastUpdateCheck = System.currentTimeMillis()
|
lastUpdateCheck = System.currentTimeMillis()
|
||||||
if (datePosted != null
|
if (datePosted != null &&
|
||||||
&& lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) {
|
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) {
|
||||||
aged = true
|
aged = true
|
||||||
XLog.d("aged %s - too old", title)
|
XLog.d("aged %s - too old", title)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Parse ratings
|
// Parse ratings
|
||||||
ignore {
|
ignore {
|
||||||
averageRating = select("#rating_label")
|
averageRating = select("#rating_label")
|
||||||
.text()
|
.text()
|
||||||
@ -428,7 +448,7 @@ class EHentai(override val id: Long,
|
|||||||
?.toInt()
|
?.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
//Parse tags
|
// Parse tags
|
||||||
tags.clear()
|
tags.clear()
|
||||||
select("#taglist tr").forEach {
|
select("#taglist tr").forEach {
|
||||||
val namespace = it.select(".tc").text().removeSuffix(":")
|
val namespace = it.select(".tc").text().removeSuffix(":")
|
||||||
@ -465,7 +485,7 @@ class EHentai(override val id: Long,
|
|||||||
fun realImageUrlParse(response: Response, page: Page): String {
|
fun realImageUrlParse(response: Response, page: Page): String {
|
||||||
with(response.asJsoup()) {
|
with(response.asJsoup()) {
|
||||||
val currentImage = getElementById("img").attr("src")
|
val currentImage = getElementById("img").attr("src")
|
||||||
//Each press of the retry button will choose another server
|
// Each press of the retry button will choose another server
|
||||||
select("#loadfail").attr("onclick").nullIfBlank()?.let {
|
select("#loadfail").attr("onclick").nullIfBlank()?.let {
|
||||||
page.url = addParam(page.url, "nl", it.substring(it.indexOf('\'') + 1 until it.lastIndexOf('\'')))
|
page.url = addParam(page.url, "nl", it.substring(it.indexOf('\'') + 1 until it.lastIndexOf('\'')))
|
||||||
}
|
}
|
||||||
@ -490,17 +510,17 @@ class EHentai(override val id: Long,
|
|||||||
cache = false)).execute()
|
cache = false)).execute()
|
||||||
val doc = response2.asJsoup()
|
val doc = response2.asJsoup()
|
||||||
|
|
||||||
//Parse favorites
|
// Parse favorites
|
||||||
val parsed = extendedGenericMangaParse(doc)
|
val parsed = extendedGenericMangaParse(doc)
|
||||||
result += parsed.first
|
result += parsed.first
|
||||||
|
|
||||||
//Parse fav names
|
// Parse fav names
|
||||||
if (favNames == null)
|
if (favNames == null)
|
||||||
favNames = doc.select(".fp:not(.fps)").mapNotNull {
|
favNames = doc.select(".fp:not(.fps)").mapNotNull {
|
||||||
it.child(2).text()
|
it.child(2).text()
|
||||||
}
|
}
|
||||||
|
|
||||||
//Next page
|
// Next page
|
||||||
page++
|
page++
|
||||||
} while (parsed.second)
|
} while (parsed.second)
|
||||||
|
|
||||||
@ -544,7 +564,7 @@ class EHentai(override val id: Long,
|
|||||||
|
|
||||||
fun cookiesHeader(sp: Int = spPref().getOrDefault()) = buildCookies(rawCookies(sp))
|
fun cookiesHeader(sp: Int = spPref().getOrDefault()) = buildCookies(rawCookies(sp))
|
||||||
|
|
||||||
//Headers
|
// Headers
|
||||||
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())!!
|
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())!!
|
||||||
|
|
||||||
fun addParam(url: String, param: String, value: String) = Uri.parse(url)
|
fun addParam(url: String, param: String, value: String) = Uri.parse(url)
|
||||||
@ -565,7 +585,7 @@ class EHentai(override val id: Long,
|
|||||||
chain.proceed(newReq)
|
chain.proceed(newReq)
|
||||||
}.build()!!
|
}.build()!!
|
||||||
|
|
||||||
//Filters
|
// Filters
|
||||||
override fun getFilterList() = FilterList(
|
override fun getFilterList() = FilterList(
|
||||||
Watched(),
|
Watched(),
|
||||||
GenreGroup(),
|
GenreGroup(),
|
||||||
@ -673,11 +693,11 @@ class EHentai(override val id: Long,
|
|||||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||||
return when (uri.pathSegments.firstOrNull()) {
|
return when (uri.pathSegments.firstOrNull()) {
|
||||||
"g" -> {
|
"g" -> {
|
||||||
//Is already gallery page, do nothing
|
// Is already gallery page, do nothing
|
||||||
uri.toString()
|
uri.toString()
|
||||||
}
|
}
|
||||||
"s" -> {
|
"s" -> {
|
||||||
//Is page, fetch gallery token and use that
|
// Is page, fetch gallery token and use that
|
||||||
getGalleryUrlFromPage(uri)
|
getGalleryUrlFromPage(uri)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
@ -713,7 +733,6 @@ class EHentai(override val id: Long,
|
|||||||
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
|
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
||||||
private const val TR_SUFFIX = "TR"
|
private const val TR_SUFFIX = "TR"
|
||||||
@ -738,6 +757,5 @@ class EHentai(override val id: Long,
|
|||||||
fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ") {
|
fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ") {
|
||||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
@ -24,6 +28,8 @@ import exh.metadata.metadata.HitomiSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
|||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
@ -32,8 +38,6 @@ import rx.Observable
|
|||||||
import rx.Single
|
import rx.Single
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Man, I hate this source :(
|
* Man, I hate this source :(
|
||||||
@ -61,8 +65,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
|||||||
private var tagIndexVersionCacheTime: Long = 0
|
private var tagIndexVersionCacheTime: Long = 0
|
||||||
private fun tagIndexVersion(): Single<Long> {
|
private fun tagIndexVersion(): Single<Long> {
|
||||||
val sCachedTagIndexVersion = cachedTagIndexVersion
|
val sCachedTagIndexVersion = cachedTagIndexVersion
|
||||||
return if (sCachedTagIndexVersion == null
|
return if (sCachedTagIndexVersion == null ||
|
||||||
|| tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
||||||
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
|
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||||
cachedTagIndexVersion = it
|
cachedTagIndexVersion = it
|
||||||
tagIndexVersionCacheTime = System.currentTimeMillis()
|
tagIndexVersionCacheTime = System.currentTimeMillis()
|
||||||
@ -76,8 +80,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
|||||||
private var galleryIndexVersionCacheTime: Long = 0
|
private var galleryIndexVersionCacheTime: Long = 0
|
||||||
private fun galleryIndexVersion(): Single<Long> {
|
private fun galleryIndexVersion(): Single<Long> {
|
||||||
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
|
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
|
||||||
return if (sCachedGalleryIndexVersion == null
|
return if (sCachedGalleryIndexVersion == null ||
|
||||||
|| galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
|
||||||
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
|
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
|
||||||
cachedGalleryIndexVersion = it
|
cachedGalleryIndexVersion = it
|
||||||
galleryIndexVersionCacheTime = System.currentTimeMillis()
|
galleryIndexVersionCacheTime = System.currentTimeMillis()
|
||||||
@ -307,7 +311,6 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||||
* override this method.
|
* override this method.
|
||||||
@ -423,5 +426,4 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
|
|||||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss'-05'", Locale.US)
|
SimpleDateFormat("yyyy-MM-dd HH:mm:ss'-05'", Locale.US)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,22 @@ package eu.kanade.tachiyomi.source.online.all
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.get
|
||||||
|
import com.github.salomonbrys.kotson.nullArray
|
||||||
|
import com.github.salomonbrys.kotson.nullLong
|
||||||
|
import com.github.salomonbrys.kotson.nullObj
|
||||||
|
import com.github.salomonbrys.kotson.nullString
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
@ -30,8 +39,8 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
|||||||
override val metaClass = NHentaiSearchMetadata::class
|
override val metaClass = NHentaiSearchMetadata::class
|
||||||
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
//TODO There is currently no way to get the most popular mangas
|
// TODO There is currently no way to get the most popular mangas
|
||||||
//TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen
|
// TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen
|
||||||
return fetchLatestUpdates(page)
|
return fetchLatestUpdates(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +48,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
|||||||
|
|
||||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
//Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
val trimmedIdQuery = query.trim().removePrefix("id:")
|
val trimmedIdQuery = query.trim().removePrefix("id:")
|
||||||
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
|
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
|
||||||
@ -246,7 +255,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
|||||||
|
|
||||||
override fun getFilterList() = FilterList(SortFilter(), filterLang())
|
override fun getFilterList() = FilterList(SortFilter(), filterLang())
|
||||||
|
|
||||||
//language filtering
|
// language filtering
|
||||||
private class filterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
|
private class filterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
|
||||||
|
|
||||||
class SortFilter : Filter.Sort(
|
class SortFilter : Filter.Sort(
|
||||||
@ -305,7 +314,6 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
|
|||||||
Pair("Chinese", " chinese")
|
Pair("Chinese", " chinese")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
val jsonParser by lazy {
|
val jsonParser by lazy {
|
||||||
JsonParser()
|
JsonParser()
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.source.online.all
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
@ -17,14 +22,15 @@ import exh.metadata.metadata.base.RaisedTag
|
|||||||
import exh.util.UriFilter
|
import exh.util.UriFilter
|
||||||
import exh.util.UriGroup
|
import exh.util.UriGroup
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
// TODO Transform into delegated source
|
// TODO Transform into delegated source
|
||||||
class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSource(),
|
class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSource(),
|
||||||
@ -54,7 +60,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
|||||||
|
|
||||||
override fun popularMangaNextPageSelector(): String? = null
|
override fun popularMangaNextPageSelector(): String? = null
|
||||||
|
|
||||||
//Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(query) {
|
urlImportFetchSearchManga(query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
@ -256,7 +262,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Explicit type arg for listOf() to workaround this: KT-16570
|
// Explicit type arg for listOf() to workaround this: KT-16570
|
||||||
class ReleaseYearGroup : UriGroup<Filter<*>>("Release Year", listOf(
|
class ReleaseYearGroup : UriGroup<Filter<*>>("Release Year", listOf(
|
||||||
ReleaseYearRangeFilter(),
|
ReleaseYearRangeFilter(),
|
||||||
ReleaseYearYearFilter()
|
ReleaseYearYearFilter()
|
||||||
|
@ -2,10 +2,14 @@ package eu.kanade.tachiyomi.source.online.english
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.kizitonwose.time.hours
|
import com.kizitonwose.time.hours
|
||||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
@ -17,13 +21,18 @@ import exh.util.CachedField
|
|||||||
import exh.util.NakedTrie
|
import exh.util.NakedTrie
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
|
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.rx2.asSingle
|
import kotlinx.coroutines.rx2.asSingle
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.*
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
@ -4,12 +4,16 @@ import android.net.Uri
|
|||||||
import com.github.salomonbrys.kotson.array
|
import com.github.salomonbrys.kotson.array
|
||||||
import com.github.salomonbrys.kotson.string
|
import com.github.salomonbrys.kotson.string
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
import eu.kanade.tachiyomi.network.asObservable
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
@ -23,7 +27,9 @@ import exh.search.Text
|
|||||||
import exh.util.await
|
import exh.util.await
|
||||||
import exh.util.dropBlank
|
import exh.util.dropBlank
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
|
import hu.akarnokd.rxjava.interop.RxJavaInterop
|
||||||
import info.debatty.java.stringsimilarity.Levenshtein
|
import info.debatty.java.stringsimilarity.Levenshtein
|
||||||
|
import kotlin.math.ceil
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@ -36,7 +42,6 @@ import org.jsoup.nodes.Document
|
|||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource {
|
class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource {
|
||||||
/**
|
/**
|
||||||
@ -182,7 +187,6 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
"/result"
|
"/result"
|
||||||
} else {
|
} else {
|
||||||
"/search"
|
"/search"
|
||||||
|
@ -30,7 +30,7 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
|||||||
*/
|
*/
|
||||||
override val metaClass = HentaiCafeSearchMetadata::class
|
override val metaClass = HentaiCafeSearchMetadata::class
|
||||||
|
|
||||||
//Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(query) {
|
urlImportFetchSearchManga(query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
|
@ -29,7 +29,7 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
|||||||
*/
|
*/
|
||||||
override val metaClass = PururinSearchMetadata::class
|
override val metaClass = PururinSearchMetadata::class
|
||||||
|
|
||||||
//Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
val trimmedIdQuery = query.trim().removePrefix("id:")
|
val trimmedIdQuery = query.trim().removePrefix("id:")
|
||||||
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
|
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
|
||||||
@ -106,4 +106,4 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
|||||||
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
override fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||||
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
|
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,32 +4,28 @@ import android.net.Uri
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LewdSource
|
import eu.kanade.tachiyomi.source.online.LewdSource
|
||||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||||
import eu.kanade.tachiyomi.util.system.asJsoup
|
import eu.kanade.tachiyomi.util.system.asJsoup
|
||||||
import exh.metadata.metadata.TsuminoSearchMetadata
|
import exh.metadata.metadata.TsuminoSearchMetadata
|
||||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.BASE_URL
|
|
||||||
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
|
||||||
import exh.metadata.metadata.base.RaisedTag
|
import exh.metadata.metadata.base.RaisedTag
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.util.dropBlank
|
|
||||||
import exh.util.trimAll
|
|
||||||
import exh.util.urlImportFetchSearchManga
|
import exh.util.urlImportFetchSearchManga
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
|
||||||
LewdSource<TsuminoSearchMetadata, Document>, UrlImportableSource {
|
LewdSource<TsuminoSearchMetadata, Document>, UrlImportableSource {
|
||||||
override val metaClass = TsuminoSearchMetadata::class;
|
override val metaClass = TsuminoSearchMetadata::class
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
//Support direct URL importing
|
// Support direct URL importing
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
urlImportFetchSearchManga(query) {
|
urlImportFetchSearchManga(query) {
|
||||||
super.fetchSearchManga(page, query, filters)
|
super.fetchSearchManga(page, query, filters)
|
||||||
|
@ -7,4 +7,4 @@ interface TabbedController {
|
|||||||
fun configureTabs(tabs: TabLayout) {}
|
fun configureTabs(tabs: TabLayout) {}
|
||||||
|
|
||||||
fun cleanupTabs(tabs: TabLayout) {}
|
fun cleanupTabs(tabs: TabLayout) {}
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,4 @@ abstract class BaseViewHolder(view: View) : androidx.recyclerview.widget.Recycle
|
|||||||
|
|
||||||
override val containerView: View?
|
override val containerView: View?
|
||||||
get() = itemView
|
get() = itemView
|
||||||
}
|
}
|
||||||
|
@ -42,4 +42,4 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
|
|||||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||||
val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
|
val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,4 +54,4 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
|
|||||||
val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
|
val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
|
||||||
val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
|
val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
@ -5,21 +5,24 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
|||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
|
||||||
import exh.isLewdSource
|
import exh.isLewdSource
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
import exh.search.SearchEngine
|
import exh.search.SearchEngine
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import exh.util.cancellable
|
import exh.util.cancellable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter storing a list of manga in a certain category.
|
* Adapter storing a list of manga in a certain category.
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
|
|
||||||
@ -26,7 +26,6 @@ abstract class LibraryHolder(
|
|||||||
*/
|
*/
|
||||||
abstract fun onSetValues(item: LibraryItem)
|
abstract fun onSetValues(item: LibraryItem)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item is released.
|
* Called when an item is released.
|
||||||
*
|
*
|
||||||
@ -36,5 +35,4 @@ abstract class LibraryHolder(
|
|||||||
super.onItemReleased(position)
|
super.onItemReleased(position)
|
||||||
(adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position)
|
(adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -235,4 +235,3 @@ class MangaInfoPresenter(
|
|||||||
return toInsert
|
return toInsert
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,4 +46,4 @@ class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.Holde
|
|||||||
section_text.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS)
|
section_text.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,17 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.util.preference.*
|
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||||
|
import eu.kanade.tachiyomi.util.preference.entriesRes
|
||||||
|
import eu.kanade.tachiyomi.util.preference.intListPreference
|
||||||
|
import eu.kanade.tachiyomi.util.preference.listPreference
|
||||||
|
import eu.kanade.tachiyomi.util.preference.multiSelectListPreference
|
||||||
|
import eu.kanade.tachiyomi.util.preference.onChange
|
||||||
|
import eu.kanade.tachiyomi.util.preference.onClick
|
||||||
|
import eu.kanade.tachiyomi.util.preference.preference
|
||||||
|
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||||
|
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||||
|
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EXH_SOURCE_ID
|
import exh.EXH_SOURCE_ID
|
||||||
@ -34,6 +44,7 @@ import exh.ui.login.LoginController
|
|||||||
import exh.util.await
|
import exh.util.await
|
||||||
import exh.util.trans
|
import exh.util.trans
|
||||||
import humanize.Humanize
|
import humanize.Humanize
|
||||||
|
import java.util.Date
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -41,7 +52,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EH Settings fragment
|
* EH Settings fragment
|
||||||
@ -52,17 +62,17 @@ class SettingsEhController : SettingsController() {
|
|||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
private fun Preference<*>.reconfigure(): Boolean {
|
private fun Preference<*>.reconfigure(): Boolean {
|
||||||
//Listen for change commit
|
// Listen for change commit
|
||||||
asObservable()
|
asObservable()
|
||||||
.skip(1) //Skip first as it is emitted immediately
|
.skip(1) // Skip first as it is emitted immediately
|
||||||
.take(1) //Only listen for first commit
|
.take(1) // Only listen for first commit
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeUntilDestroy {
|
.subscribeUntilDestroy {
|
||||||
//Only listen for first change commit
|
// Only listen for first change commit
|
||||||
WarnConfigureDialogController.uploadSettings(router)
|
WarnConfigureDialogController.uploadSettings(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Always return true to save changes
|
// Always return true to save changes
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,4 +23,4 @@ fun Element.attrOrText(css: String): String {
|
|||||||
*/
|
*/
|
||||||
fun Response.asJsoup(html: String? = null): Document {
|
fun Response.asJsoup(html: String? = null): Document {
|
||||||
return Jsoup.parse(html ?: body!!.string(), request.url.toString())
|
return Jsoup.parse(html ?: body!!.string(), request.url.toString())
|
||||||
}
|
}
|
||||||
|
@ -16,4 +16,4 @@ fun ImageView.setVectorCompat(@DrawableRes drawable: Int, tint: Int? = null) {
|
|||||||
vector?.setTint(tint)
|
vector?.setTint(tint)
|
||||||
}
|
}
|
||||||
setImageDrawable(vector)
|
setImageDrawable(vector)
|
||||||
}
|
}
|
||||||
|
@ -12,4 +12,4 @@ import androidx.annotation.LayoutRes
|
|||||||
*/
|
*/
|
||||||
fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View {
|
fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View {
|
||||||
return LayoutInflater.from(context).inflate(layout, this, attachToRoot)
|
return LayoutInflater.from(context).inflate(layout, this, attachToRoot)
|
||||||
}
|
}
|
||||||
|
@ -23,4 +23,4 @@ class IntListPreference @JvmOverloads constructor(context: Context, attrs: Attri
|
|||||||
defaultReturnValue
|
defaultReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ private val lewdDelegatedSourceIds = SourceManager.DELEGATED_SOURCES.filter {
|
|||||||
}.map { it.value.sourceId }.sorted()
|
}.map { it.value.sourceId }.sorted()
|
||||||
|
|
||||||
// This method MUST be fast!
|
// This method MUST be fast!
|
||||||
fun isLewdSource(source: Long) = source in 6900..6999
|
fun isLewdSource(source: Long) = source in 6900..6999 ||
|
||||||
|| lewdDelegatedSourceIds.binarySearch(source) >= 0
|
lewdDelegatedSourceIds.binarySearch(source) >= 0
|
||||||
|
|
||||||
fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID
|
fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID
|
||||||
|
@ -2,6 +2,8 @@ package exh
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
|
||||||
data class EXHSavedSearch(val name: String,
|
data class EXHSavedSearch(
|
||||||
val query: String,
|
val name: String,
|
||||||
val filterList: FilterList)
|
val query: String,
|
||||||
|
val filterList: FilterList
|
||||||
|
)
|
||||||
|
@ -16,20 +16,22 @@ class GalleryAdder {
|
|||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
fun addGallery(url: String,
|
fun addGallery(
|
||||||
fav: Boolean = false,
|
url: String,
|
||||||
forceSource: UrlImportableSource? = null,
|
fav: Boolean = false,
|
||||||
throttleFunc: () -> Unit = {}): GalleryAddEvent {
|
forceSource: UrlImportableSource? = null,
|
||||||
|
throttleFunc: () -> Unit = {}
|
||||||
|
): GalleryAddEvent {
|
||||||
XLog.d("Importing gallery (url: %s, fav: %s, forceSource: %s)...", url, fav, forceSource)
|
XLog.d("Importing gallery (url: %s, fav: %s, forceSource: %s)...", url, fav, forceSource)
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
|
|
||||||
// Find matching source
|
// Find matching source
|
||||||
val source = if(forceSource != null) {
|
val source = if (forceSource != null) {
|
||||||
try {
|
try {
|
||||||
if (forceSource.matchesUri(uri)) forceSource
|
if (forceSource.matchesUri(uri)) forceSource
|
||||||
else return GalleryAddEvent.Fail.UnknownType(url)
|
else return GalleryAddEvent.Fail.UnknownType(url)
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
XLog.e("Source URI match check error!", e)
|
XLog.e("Source URI match check error!", e)
|
||||||
return GalleryAddEvent.Fail.UnknownType(url)
|
return GalleryAddEvent.Fail.UnknownType(url)
|
||||||
}
|
}
|
||||||
@ -39,7 +41,7 @@ class GalleryAdder {
|
|||||||
.find {
|
.find {
|
||||||
try {
|
try {
|
||||||
it.matchesUri(uri)
|
it.matchesUri(uri)
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
XLog.e("Source URI match check error!", e)
|
XLog.e("Source URI match check error!", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -49,7 +51,7 @@ class GalleryAdder {
|
|||||||
// Map URL to manga URL
|
// Map URL to manga URL
|
||||||
val realUrl = try {
|
val realUrl = try {
|
||||||
source.mapUrlToMangaUrl(uri)
|
source.mapUrlToMangaUrl(uri)
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
XLog.e("Source URI map-to-manga error!", e)
|
XLog.e("Source URI map-to-manga error!", e)
|
||||||
null
|
null
|
||||||
} ?: return GalleryAddEvent.Fail.UnknownType(url)
|
} ?: return GalleryAddEvent.Fail.UnknownType(url)
|
||||||
@ -57,12 +59,12 @@ class GalleryAdder {
|
|||||||
// Clean URL
|
// Clean URL
|
||||||
val cleanedUrl = try {
|
val cleanedUrl = try {
|
||||||
source.cleanMangaUrl(realUrl)
|
source.cleanMangaUrl(realUrl)
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
XLog.e("Source URI clean error!", e)
|
XLog.e("Source URI clean error!", e)
|
||||||
null
|
null
|
||||||
} ?: return GalleryAddEvent.Fail.UnknownType(url)
|
} ?: return GalleryAddEvent.Fail.UnknownType(url)
|
||||||
|
|
||||||
//Use manga in DB if possible, otherwise, make a new manga
|
// Use manga in DB if possible, otherwise, make a new manga
|
||||||
val manga = db.getManga(cleanedUrl, source.id).executeAsBlocking()
|
val manga = db.getManga(cleanedUrl, source.id).executeAsBlocking()
|
||||||
?: Manga.create(source.id).apply {
|
?: Manga.create(source.id).apply {
|
||||||
this.url = cleanedUrl
|
this.url = cleanedUrl
|
||||||
@ -71,7 +73,7 @@ class GalleryAdder {
|
|||||||
|
|
||||||
// Insert created manga if not in DB before fetching details
|
// Insert created manga if not in DB before fetching details
|
||||||
// This allows us to keep the metadata when fetching details
|
// This allows us to keep the metadata when fetching details
|
||||||
if(manga.id == null) {
|
if (manga.id == null) {
|
||||||
db.insertManga(manga).executeAsBlocking().insertedId()?.let {
|
db.insertManga(manga).executeAsBlocking().insertedId()?.let {
|
||||||
manga.id = it
|
manga.id = it
|
||||||
}
|
}
|
||||||
@ -86,9 +88,9 @@ class GalleryAdder {
|
|||||||
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
|
||||||
//Fetch and copy chapters
|
// Fetch and copy chapters
|
||||||
try {
|
try {
|
||||||
val chapterListObs = if(source is EHentai) {
|
val chapterListObs = if (source is EHentai) {
|
||||||
source.fetchChapterList(manga, throttleFunc)
|
source.fetchChapterList(manga, throttleFunc)
|
||||||
} else {
|
} else {
|
||||||
source.fetchChapterList(manga)
|
source.fetchChapterList(manga)
|
||||||
@ -102,10 +104,10 @@ class GalleryAdder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return GalleryAddEvent.Success(url, manga)
|
return GalleryAddEvent.Success(url, manga)
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
XLog.w("Could not add gallery (url: $url)!", e)
|
XLog.w("Could not add gallery (url: $url)!", e)
|
||||||
|
|
||||||
if(e is EHentai.GalleryNotFoundException) {
|
if (e is EHentai.GalleryNotFoundException) {
|
||||||
return GalleryAddEvent.Fail.NotFound(url)
|
return GalleryAddEvent.Fail.NotFound(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,21 +122,25 @@ sealed class GalleryAddEvent {
|
|||||||
abstract val galleryUrl: String
|
abstract val galleryUrl: String
|
||||||
open val galleryTitle: String? = null
|
open val galleryTitle: String? = null
|
||||||
|
|
||||||
class Success(override val galleryUrl: String,
|
class Success(
|
||||||
val manga: Manga): GalleryAddEvent() {
|
override val galleryUrl: String,
|
||||||
|
val manga: Manga
|
||||||
|
) : GalleryAddEvent() {
|
||||||
override val galleryTitle = manga.title
|
override val galleryTitle = manga.title
|
||||||
override val logMessage = "Added gallery: $galleryTitle"
|
override val logMessage = "Added gallery: $galleryTitle"
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Fail: GalleryAddEvent() {
|
sealed class Fail : GalleryAddEvent() {
|
||||||
class UnknownType(override val galleryUrl: String): Fail() {
|
class UnknownType(override val galleryUrl: String) : Fail() {
|
||||||
override val logMessage = "Unknown gallery type for gallery: $galleryUrl"
|
override val logMessage = "Unknown gallery type for gallery: $galleryUrl"
|
||||||
}
|
}
|
||||||
|
|
||||||
open class Error(override val galleryUrl: String,
|
open class Error(
|
||||||
override val logMessage: String): Fail()
|
override val galleryUrl: String,
|
||||||
|
override val logMessage: String
|
||||||
|
) : Fail()
|
||||||
|
|
||||||
class NotFound(galleryUrl: String):
|
class NotFound(galleryUrl: String) :
|
||||||
Error(galleryUrl, "Gallery does not exist: $galleryUrl")
|
Error(galleryUrl, "Gallery does not exist: $galleryUrl")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,20 +9,19 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.util.system.jobScheduler
|
import eu.kanade.tachiyomi.util.system.jobScheduler
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EXH_SOURCE_ID
|
|
||||||
import exh.EXHMigrations
|
import exh.EXHMigrations
|
||||||
|
import exh.EXH_SOURCE_ID
|
||||||
import exh.eh.EHentaiUpdateWorker
|
import exh.eh.EHentaiUpdateWorker
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
import exh.metadata.metadata.base.insertFlatMetadata
|
import exh.metadata.metadata.base.insertFlatMetadata
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import exh.util.cancellable
|
import exh.util.cancellable
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
object DebugFunctions {
|
object DebugFunctions {
|
||||||
val app: Application by injectLazy()
|
val app: Application by injectLazy()
|
||||||
@ -31,7 +30,7 @@ object DebugFunctions {
|
|||||||
val sourceManager: SourceManager by injectLazy()
|
val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
fun forceUpgradeMigration() {
|
fun forceUpgradeMigration() {
|
||||||
prefs.eh_lastVersionCode().set(0)
|
prefs.eh_lastVersionCode().set(0)
|
||||||
EXHMigrations.upgrade(prefs)
|
EXHMigrations.upgrade(prefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +46,7 @@ object DebugFunctions {
|
|||||||
|
|
||||||
for (manga in allManga) {
|
for (manga in allManga) {
|
||||||
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
|
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
|
||||||
if(meta != null) {
|
if (meta != null) {
|
||||||
// remove age flag
|
// remove age flag
|
||||||
meta.aged = false
|
meta.aged = false
|
||||||
db.insertFlatMetadata(meta.flatten()).await()
|
db.insertFlatMetadata(meta.flatten()).await()
|
||||||
|
@ -26,4 +26,4 @@ enum class DebugToggles(val default: Boolean) {
|
|||||||
companion object {
|
companion object {
|
||||||
private val prefs: PreferencesHelper by injectLazy()
|
private val prefs: PreferencesHelper by injectLazy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,12 @@ import android.widget.HorizontalScrollView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import eu.kanade.tachiyomi.ui.setting.*
|
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
||||||
import eu.kanade.tachiyomi.util.preference.*
|
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||||
|
import eu.kanade.tachiyomi.util.preference.onClick
|
||||||
|
import eu.kanade.tachiyomi.util.preference.preference
|
||||||
|
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||||
|
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||||
import kotlin.reflect.KVisibility
|
import kotlin.reflect.KVisibility
|
||||||
import kotlin.reflect.full.declaredFunctions
|
import kotlin.reflect.full.declaredFunctions
|
||||||
|
|
||||||
@ -41,7 +45,7 @@ class SettingsDebugController : SettingsController() {
|
|||||||
view.text = "Function returned result:\n\n$result"
|
view.text = "Function returned result:\n\n$result"
|
||||||
MaterialDialog.Builder(context)
|
MaterialDialog.Builder(context)
|
||||||
.customView(hView, true)
|
.customView(hView, true)
|
||||||
} catch(t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}"
|
view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}"
|
||||||
MaterialDialog.Builder(context)
|
MaterialDialog.Builder(context)
|
||||||
.customView(hView, true)
|
.customView(hView, true)
|
||||||
@ -59,8 +63,8 @@ class SettingsDebugController : SettingsController() {
|
|||||||
title = it.name.replace('_', ' ').toLowerCase().capitalize()
|
title = it.name.replace('_', ' ').toLowerCase().capitalize()
|
||||||
key = it.prefKey
|
key = it.prefKey
|
||||||
defaultValue = it.default
|
defaultValue = it.default
|
||||||
summaryOn = if(it.default) "" else MODIFIED_TEXT
|
summaryOn = if (it.default) "" else MODIFIED_TEXT
|
||||||
summaryOff = if(it.default) MODIFIED_TEXT else ""
|
summaryOff = if (it.default) MODIFIED_TEXT else ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,4 +78,4 @@ class SettingsDebugController : SettingsController() {
|
|||||||
companion object {
|
companion object {
|
||||||
private val MODIFIED_TEXT = Html.fromHtml("<font color='red'>MODIFIED</font>")
|
private val MODIFIED_TEXT = Html.fromHtml("<font color='red'>MODIFIED</font>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
package exh.eh
|
package exh.eh
|
||||||
|
|
||||||
class EHentaiThrottleManager(private val max: Int = THROTTLE_MAX,
|
class EHentaiThrottleManager(
|
||||||
private val inc: Int = THROTTLE_INC) {
|
private val max: Int = THROTTLE_MAX,
|
||||||
|
private val inc: Int = THROTTLE_INC
|
||||||
|
) {
|
||||||
private var lastThrottleTime: Long = 0
|
private var lastThrottleTime: Long = 0
|
||||||
var throttleTime: Long = 0
|
var throttleTime: Long = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun throttle() {
|
fun throttle() {
|
||||||
//Throttle requests if necessary
|
// Throttle requests if necessary
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val timeDiff = now - lastThrottleTime
|
val timeDiff = now - lastThrottleTime
|
||||||
if(timeDiff < throttleTime)
|
if (timeDiff < throttleTime)
|
||||||
Thread.sleep(throttleTime - timeDiff)
|
Thread.sleep(throttleTime - timeDiff)
|
||||||
|
|
||||||
if(throttleTime < max)
|
if (throttleTime < max)
|
||||||
throttleTime += inc
|
throttleTime += inc
|
||||||
|
|
||||||
lastThrottleTime = System.currentTimeMillis()
|
lastThrottleTime = System.currentTimeMillis()
|
||||||
@ -28,4 +30,4 @@ class EHentaiThrottleManager(private val max: Int = THROTTLE_MAX,
|
|||||||
const val THROTTLE_MAX = 5500
|
const val THROTTLE_MAX = 5500
|
||||||
const val THROTTLE_INC = 20
|
const val THROTTLE_INC = 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
|
import java.io.File
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
data class ChapterChain(val manga: Manga, val chapters: List<Chapter>)
|
data class ChapterChain(val manga: Manga, val chapters: List<Chapter>)
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ class EHentaiUpdateHelper(context: Context) {
|
|||||||
|
|
||||||
val chainsAsChapters = chains.flatMap { it.chapters }
|
val chainsAsChapters = chains.flatMap { it.chapters }
|
||||||
|
|
||||||
if(toDiscard.isNotEmpty()) {
|
if (toDiscard.isNotEmpty()) {
|
||||||
var new = false
|
var new = false
|
||||||
|
|
||||||
// Copy chain chapters to curChapters
|
// Copy chain chapters to curChapters
|
||||||
@ -75,9 +75,9 @@ class EHentaiUpdateHelper(context: Context) {
|
|||||||
|
|
||||||
chain.chapters.map { chapter ->
|
chain.chapters.map { chapter ->
|
||||||
// Convert old style chapters to new style chapters if possible
|
// Convert old style chapters to new style chapters if possible
|
||||||
if(chapter.date_upload <= 0
|
if (chapter.date_upload <= 0 &&
|
||||||
&& meta?.datePosted != null
|
meta?.datePosted != null &&
|
||||||
&& meta?.title != null) {
|
meta?.title != null) {
|
||||||
chapter.name = meta!!.title!!
|
chapter.name = meta!!.title!!
|
||||||
chapter.date_upload = meta!!.datePosted!!
|
chapter.date_upload = meta!!.datePosted!!
|
||||||
}
|
}
|
||||||
@ -92,7 +92,7 @@ class EHentaiUpdateHelper(context: Context) {
|
|||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
existing.read = existing.read || chapter.read
|
existing.read = existing.read || chapter.read
|
||||||
existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read)
|
existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read)
|
||||||
if(newLastPageRead != null && existing.last_page_read <= 0) {
|
if (newLastPageRead != null && existing.last_page_read <= 0) {
|
||||||
existing.last_page_read = newLastPageRead
|
existing.last_page_read = newLastPageRead
|
||||||
}
|
}
|
||||||
existing.bookmark = existing.bookmark || chapter.bookmark
|
existing.bookmark = existing.bookmark || chapter.bookmark
|
||||||
@ -107,7 +107,7 @@ class EHentaiUpdateHelper(context: Context) {
|
|||||||
bookmark = chapter.bookmark
|
bookmark = chapter.bookmark
|
||||||
|
|
||||||
last_page_read = chapter.last_page_read
|
last_page_read = chapter.last_page_read
|
||||||
if(newLastPageRead != null && last_page_read <= 0) {
|
if (newLastPageRead != null && last_page_read <= 0) {
|
||||||
last_page_read = newLastPageRead
|
last_page_read = newLastPageRead
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ class EHentaiUpdateHelper(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class GalleryEntry(val gId: String, val gToken: String) {
|
data class GalleryEntry(val gId: String, val gToken: String) {
|
||||||
class Serializer: MemAutoFlushingLookupTable.EntrySerializer<GalleryEntry> {
|
class Serializer : MemAutoFlushingLookupTable.EntrySerializer<GalleryEntry> {
|
||||||
/**
|
/**
|
||||||
* Serialize an entry as a String.
|
* Serialize an entry as a String.
|
||||||
*/
|
*/
|
||||||
|
@ -20,8 +20,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
import eu.kanade.tachiyomi.util.system.jobScheduler
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import eu.kanade.tachiyomi.util.system.jobScheduler
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EXH_SOURCE_ID
|
import exh.EXH_SOURCE_ID
|
||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
@ -31,18 +31,23 @@ import exh.metadata.metadata.base.getFlatMetadataForManga
|
|||||||
import exh.metadata.metadata.base.insertFlatMetadata
|
import exh.metadata.metadata.base.insertFlatMetadata
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import exh.util.cancellable
|
import exh.util.cancellable
|
||||||
import kotlinx.coroutines.*
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
class EHentaiUpdateWorker : JobService(), CoroutineScope {
|
||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
get() = Dispatchers.Default + Job()
|
get() = Dispatchers.Default + Job()
|
||||||
|
|
||||||
@ -215,8 +220,8 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
|||||||
val (acceptedRoot, discardedRoots, hasNew) =
|
val (acceptedRoot, discardedRoots, hasNew) =
|
||||||
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
|
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
|
||||||
|
|
||||||
if((new.isNotEmpty() && manga.id == acceptedRoot.manga.id)
|
if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) ||
|
||||||
|| (hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })) {
|
(hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })) {
|
||||||
updatedManga += acceptedRoot.manga
|
updatedManga += acceptedRoot.manga
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +240,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if(updatedManga.isNotEmpty()) {
|
if (updatedManga.isNotEmpty()) {
|
||||||
updateNotifier.showResultNotification(updatedManga)
|
updateNotifier.showResultNotification(updatedManga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,10 +259,10 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
|||||||
val newChapters = source.fetchChapterList(manga).toSingle().await(Schedulers.io())
|
val newChapters = source.fetchChapterList(manga).toSingle().await(Schedulers.io())
|
||||||
val (new, _) = syncChaptersWithSource(db, newChapters, manga, source) // Not suspending, but does block, maybe fix this?
|
val (new, _) = syncChaptersWithSource(db, newChapters, manga, source) // Not suspending, but does block, maybe fix this?
|
||||||
return new to db.getChapters(manga).await()
|
return new to db.getChapters(manga).await()
|
||||||
} catch(t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
if(t is EHentai.GalleryNotFoundException) {
|
if (t is EHentai.GalleryNotFoundException) {
|
||||||
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
|
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
|
||||||
if(meta != null) {
|
if (meta != null) {
|
||||||
// Age dead galleries
|
// Age dead galleries
|
||||||
logger.d("Aged %s - notfound", manga.id)
|
logger.d("Aged %s - notfound", manga.id)
|
||||||
meta.aged = true
|
meta.aged = true
|
||||||
@ -286,18 +291,20 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
|||||||
|
|
||||||
private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder {
|
private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder {
|
||||||
return JobInfo.Builder(
|
return JobInfo.Builder(
|
||||||
if(isTest) JOB_ID_UPDATE_BACKGROUND_TEST
|
if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST
|
||||||
else JOB_ID_UPDATE_BACKGROUND, componentName())
|
else JOB_ID_UPDATE_BACKGROUND, componentName())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.periodicBackgroundJobInfo(period: Long,
|
private fun Context.periodicBackgroundJobInfo(
|
||||||
requireCharging: Boolean,
|
period: Long,
|
||||||
requireUnmetered: Boolean): JobInfo {
|
requireCharging: Boolean,
|
||||||
|
requireUnmetered: Boolean
|
||||||
|
): JobInfo {
|
||||||
return baseBackgroundJobInfo(false)
|
return baseBackgroundJobInfo(false)
|
||||||
.setPeriodic(period)
|
.setPeriodic(period)
|
||||||
.setPersisted(true)
|
.setPersisted(true)
|
||||||
.setRequiredNetworkType(
|
.setRequiredNetworkType(
|
||||||
if(requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
|
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
|
||||||
else JobInfo.NETWORK_TYPE_ANY)
|
else JobInfo.NETWORK_TYPE_ANY)
|
||||||
.apply {
|
.apply {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@ -321,7 +328,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
|||||||
|
|
||||||
fun launchBackgroundTest(context: Context) {
|
fun launchBackgroundTest(context: Context) {
|
||||||
val jobScheduler = context.jobScheduler
|
val jobScheduler = context.jobScheduler
|
||||||
if(jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) {
|
if (jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) {
|
||||||
logger.e("Failed to schedule background test job!")
|
logger.e("Failed to schedule background test job!")
|
||||||
} else {
|
} else {
|
||||||
logger.d("Successfully scheduled background test job!")
|
logger.d("Successfully scheduled background test job!")
|
||||||
@ -344,7 +351,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
|
|||||||
wifiRestriction
|
wifiRestriction
|
||||||
)
|
)
|
||||||
|
|
||||||
if(context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
|
if (context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
|
||||||
logger.e("Failed to schedule background update job!")
|
logger.e("Failed to schedule background update job!")
|
||||||
} else {
|
} else {
|
||||||
logger.d("Successfully scheduled background update job!")
|
logger.d("Successfully scheduled background update job!")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package exh.eh
|
package exh.eh
|
||||||
|
|
||||||
data class EHentaiUpdaterStats(
|
data class EHentaiUpdaterStats(
|
||||||
val startTime: Long,
|
val startTime: Long,
|
||||||
val possibleUpdates: Int,
|
val possibleUpdates: Int,
|
||||||
val updateCount: Int
|
val updateCount: Int
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
package exh.eh
|
package exh.eh
|
||||||
|
|
||||||
class GalleryNotUpdatedException(val network: Boolean, cause: Throwable): RuntimeException(cause)
|
class GalleryNotUpdatedException(val network: Boolean, cause: Throwable) : RuntimeException(cause)
|
||||||
|
@ -3,9 +3,6 @@ package exh.eh
|
|||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
import androidx.core.util.AtomicFile
|
import androidx.core.util.AtomicFile
|
||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@ -13,6 +10,18 @@ import java.io.InputStream
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In memory Int -> Obj lookup table implementation that
|
* In memory Int -> Obj lookup table implementation that
|
||||||
@ -23,9 +32,9 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
* @author nulldev
|
* @author nulldev
|
||||||
*/
|
*/
|
||||||
class MemAutoFlushingLookupTable<T>(
|
class MemAutoFlushingLookupTable<T>(
|
||||||
file: File,
|
file: File,
|
||||||
private val serializer: EntrySerializer<T>,
|
private val serializer: EntrySerializer<T>,
|
||||||
private val debounceTimeMs: Long = 3000
|
private val debounceTimeMs: Long = 3000
|
||||||
) : CoroutineScope, Closeable {
|
) : CoroutineScope, Closeable {
|
||||||
/**
|
/**
|
||||||
* The context of this scope.
|
* The context of this scope.
|
||||||
@ -49,7 +58,7 @@ class MemAutoFlushingLookupTable<T>(
|
|||||||
private val atomicFile = AtomicFile(file)
|
private val atomicFile = AtomicFile(file)
|
||||||
|
|
||||||
private val shutdownHook = thread(start = false) {
|
private val shutdownHook = thread(start = false) {
|
||||||
if(!flushed) writeSynchronously()
|
if (!flushed) writeSynchronously()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -62,9 +71,9 @@ class MemAutoFlushingLookupTable<T>(
|
|||||||
var readIter = 0
|
var readIter = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
val readThisIter = read(targetArray, readIter, byteCount - readIter)
|
val readThisIter = read(targetArray, readIter, byteCount - readIter)
|
||||||
if(readThisIter <= 0) return false // No more data to read
|
if (readThisIter <= 0) return false // No more data to read
|
||||||
readIter += readThisIter
|
readIter += readThisIter
|
||||||
if(readIter == byteCount) return true
|
if (readIter == byteCount) return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,16 +83,16 @@ class MemAutoFlushingLookupTable<T>(
|
|||||||
atomicFile.openRead().buffered().use { input ->
|
atomicFile.openRead().buffered().use { input ->
|
||||||
val bb = ByteBuffer.allocate(8)
|
val bb = ByteBuffer.allocate(8)
|
||||||
|
|
||||||
while(true) {
|
while (true) {
|
||||||
if(!input.requireBytes(bb.array(), 8)) break
|
if (!input.requireBytes(bb.array(), 8)) break
|
||||||
val k = bb.getInt(0)
|
val k = bb.getInt(0)
|
||||||
val size = bb.getInt(4)
|
val size = bb.getInt(4)
|
||||||
val strBArr = ByteArray(size)
|
val strBArr = ByteArray(size)
|
||||||
if(!input.requireBytes(strBArr, size)) break
|
if (!input.requireBytes(strBArr, size)) break
|
||||||
table.put(k, serializer.read(strBArr.toString(Charsets.UTF_8)))
|
table.put(k, serializer.read(strBArr.toString(Charsets.UTF_8)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
XLog.d("Lookup table not found!", e)
|
XLog.d("Lookup table not found!", e)
|
||||||
// Ignored
|
// Ignored
|
||||||
}
|
}
|
||||||
@ -97,11 +106,11 @@ class MemAutoFlushingLookupTable<T>(
|
|||||||
flushed = false
|
flushed = false
|
||||||
launch {
|
launch {
|
||||||
delay(debounceTimeMs)
|
delay(debounceTimeMs)
|
||||||
if(id != writeCounter) return@launch
|
if (id != writeCounter) return@launch
|
||||||
|
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
// Second check inside of mutex to prevent dupe writes
|
// Second check inside of mutex to prevent dupe writes
|
||||||
if(id != writeCounter) return@launch
|
if (id != writeCounter) return@launch
|
||||||
withContext(NonCancellable) {
|
withContext(NonCancellable) {
|
||||||
writeSynchronously()
|
writeSynchronously()
|
||||||
|
|
||||||
@ -118,7 +127,7 @@ class MemAutoFlushingLookupTable<T>(
|
|||||||
val fos = atomicFile.startWrite()
|
val fos = atomicFile.startWrite()
|
||||||
try {
|
try {
|
||||||
val out = fos.buffered()
|
val out = fos.buffered()
|
||||||
for(i in 0 until table.size()) {
|
for (i in 0 until table.size()) {
|
||||||
val k = table.keyAt(i)
|
val k = table.keyAt(i)
|
||||||
val v = serializer.write(table.valueAt(i)).toByteArray(Charsets.UTF_8)
|
val v = serializer.write(table.valueAt(i)).toByteArray(Charsets.UTF_8)
|
||||||
bb.putInt(0, k)
|
bb.putInt(0, k)
|
||||||
@ -128,7 +137,7 @@ class MemAutoFlushingLookupTable<T>(
|
|||||||
}
|
}
|
||||||
out.flush()
|
out.flush()
|
||||||
atomicFile.finishWrite(fos)
|
atomicFile.finishWrite(fos)
|
||||||
} catch(t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
atomicFile.failWrite(fos)
|
atomicFile.failWrite(fos)
|
||||||
throw t
|
throw t
|
||||||
}
|
}
|
||||||
@ -212,4 +221,4 @@ class MemAutoFlushingLookupTable<T>(
|
|||||||
private const val INITIAL_SIZE = 1000
|
private const val INITIAL_SIZE = 1000
|
||||||
private const val ENTRY_SIZE_BYTES = 8
|
private const val ENTRY_SIZE_BYTES = 8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import io.realm.RealmObject
|
|||||||
import io.realm.annotations.Index
|
import io.realm.annotations.Index
|
||||||
import io.realm.annotations.PrimaryKey
|
import io.realm.annotations.PrimaryKey
|
||||||
import io.realm.annotations.RealmClass
|
import io.realm.annotations.RealmClass
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
@RealmClass
|
@RealmClass
|
||||||
open class FavoriteEntry : RealmObject() {
|
open class FavoriteEntry : RealmObject() {
|
||||||
|
@ -17,18 +17,21 @@ import eu.kanade.tachiyomi.util.lang.launchUI
|
|||||||
import eu.kanade.tachiyomi.util.system.powerManager
|
import eu.kanade.tachiyomi.util.system.powerManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.system.wifiManager
|
import eu.kanade.tachiyomi.util.system.wifiManager
|
||||||
import exh.*
|
import exh.EH_SOURCE_ID
|
||||||
|
import exh.EXH_SOURCE_ID
|
||||||
|
import exh.GalleryAddEvent
|
||||||
|
import exh.GalleryAdder
|
||||||
import exh.eh.EHentaiThrottleManager
|
import exh.eh.EHentaiThrottleManager
|
||||||
import exh.eh.EHentaiUpdateWorker
|
import exh.eh.EHentaiUpdateWorker
|
||||||
import exh.util.ignore
|
import exh.util.ignore
|
||||||
import exh.util.trans
|
import exh.util.trans
|
||||||
|
import kotlin.concurrent.thread
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import rx.subjects.BehaviorSubject
|
import rx.subjects.BehaviorSubject
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
class FavoritesSyncHelper(val context: Context) {
|
class FavoritesSyncHelper(val context: Context) {
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
@ -55,7 +58,7 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun runSync() {
|
fun runSync() {
|
||||||
if(status.value !is FavoritesSyncStatus.Idle) {
|
if (status.value !is FavoritesSyncStatus.Idle) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +68,8 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun beginSync() {
|
private fun beginSync() {
|
||||||
//Check if logged in
|
// Check if logged in
|
||||||
if(!prefs.enableExhentai().getOrDefault()) {
|
if (!prefs.enableExhentai().getOrDefault()) {
|
||||||
status.onNext(FavoritesSyncStatus.Error("Please log in!"))
|
status.onNext(FavoritesSyncStatus.Error("Please log in!"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -76,9 +79,9 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||||
val seenManga = HashSet<Long>(libraryManga.size)
|
val seenManga = HashSet<Long>(libraryManga.size)
|
||||||
libraryManga.forEach {
|
libraryManga.forEach {
|
||||||
if(it.source != EXH_SOURCE_ID && it.source != EH_SOURCE_ID) return@forEach
|
if (it.source != EXH_SOURCE_ID && it.source != EH_SOURCE_ID) return@forEach
|
||||||
|
|
||||||
if(it.id in seenManga) {
|
if (it.id in seenManga) {
|
||||||
val inCategories = db.getCategoriesForManga(it).executeAsBlocking()
|
val inCategories = db.getCategoriesForManga(it).executeAsBlocking()
|
||||||
status.onNext(FavoritesSyncStatus.BadLibraryState
|
status.onNext(FavoritesSyncStatus.BadLibraryState
|
||||||
.MangaInMultipleCategories(it, inCategories))
|
.MangaInMultipleCategories(it, inCategories))
|
||||||
@ -89,20 +92,20 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Download remote favorites
|
// Download remote favorites
|
||||||
val favorites = try {
|
val favorites = try {
|
||||||
status.onNext(FavoritesSyncStatus.Processing("Downloading favorites from remote server"))
|
status.onNext(FavoritesSyncStatus.Processing("Downloading favorites from remote server"))
|
||||||
exh.fetchFavorites()
|
exh.fetchFavorites()
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
status.onNext(FavoritesSyncStatus.Error("Failed to fetch favorites from remote server!"))
|
status.onNext(FavoritesSyncStatus.Error("Failed to fetch favorites from remote server!"))
|
||||||
logger.e( "Could not fetch favorites!", e)
|
logger.e("Could not fetch favorites!", e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val errorList = mutableListOf<String>()
|
val errorList = mutableListOf<String>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//Take wake + wifi locks
|
// Take wake + wifi locks
|
||||||
ignore { wakeLock?.release() }
|
ignore { wakeLock?.release() }
|
||||||
wakeLock = ignore {
|
wakeLock = ignore {
|
||||||
context.powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
context.powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
@ -124,20 +127,20 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
status.onNext(FavoritesSyncStatus.Processing("Calculating remote changes"))
|
status.onNext(FavoritesSyncStatus.Processing("Calculating remote changes"))
|
||||||
val remoteChanges = storage.getChangedRemoteEntries(realm, favorites.first)
|
val remoteChanges = storage.getChangedRemoteEntries(realm, favorites.first)
|
||||||
val localChanges = if(prefs.eh_readOnlySync().getOrDefault()) {
|
val localChanges = if (prefs.eh_readOnlySync().getOrDefault()) {
|
||||||
null //Do not build local changes if they are not going to be applied
|
null // Do not build local changes if they are not going to be applied
|
||||||
} else {
|
} else {
|
||||||
status.onNext(FavoritesSyncStatus.Processing("Calculating local changes"))
|
status.onNext(FavoritesSyncStatus.Processing("Calculating local changes"))
|
||||||
storage.getChangedDbEntries(realm)
|
storage.getChangedDbEntries(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Apply remote categories
|
// Apply remote categories
|
||||||
status.onNext(FavoritesSyncStatus.Processing("Updating category names"))
|
status.onNext(FavoritesSyncStatus.Processing("Updating category names"))
|
||||||
applyRemoteCategories(errorList, favorites.second)
|
applyRemoteCategories(errorList, favorites.second)
|
||||||
|
|
||||||
//Apply change sets
|
// Apply change sets
|
||||||
applyChangeSetToLocal(errorList, remoteChanges)
|
applyChangeSetToLocal(errorList, remoteChanges)
|
||||||
if(localChanges != null)
|
if (localChanges != null)
|
||||||
applyChangeSetToRemote(errorList, localChanges)
|
applyChangeSetToRemote(errorList, localChanges)
|
||||||
|
|
||||||
status.onNext(FavoritesSyncStatus.Processing("Cleaning up"))
|
status.onNext(FavoritesSyncStatus.Processing("Cleaning up"))
|
||||||
@ -150,16 +153,16 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
launchUI {
|
launchUI {
|
||||||
theContext.toast("Sync complete!")
|
theContext.toast("Sync complete!")
|
||||||
}
|
}
|
||||||
} catch(e: IgnoredException) {
|
} catch (e: IgnoredException) {
|
||||||
//Do not display error as this error has already been reported
|
// Do not display error as this error has already been reported
|
||||||
logger.w( "Ignoring exception!", e)
|
logger.w("Ignoring exception!", e)
|
||||||
return
|
return
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
status.onNext(FavoritesSyncStatus.Error("Unknown error: ${e.message}"))
|
status.onNext(FavoritesSyncStatus.Error("Unknown error: ${e.message}"))
|
||||||
logger.e( "Sync error!", e)
|
logger.e("Sync error!", e)
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
//Release wake + wifi locks
|
// Release wake + wifi locks
|
||||||
ignore {
|
ignore {
|
||||||
wakeLock?.release()
|
wakeLock?.release()
|
||||||
wakeLock = null
|
wakeLock = null
|
||||||
@ -175,7 +178,7 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(errorList.isEmpty())
|
if (errorList.isEmpty())
|
||||||
status.onNext(FavoritesSyncStatus.Idle())
|
status.onNext(FavoritesSyncStatus.Idle())
|
||||||
else
|
else
|
||||||
status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList))
|
status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList))
|
||||||
@ -195,31 +198,31 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
Category.create(remote).apply {
|
Category.create(remote).apply {
|
||||||
order = index
|
order = index
|
||||||
|
|
||||||
//Going through categories list from front to back
|
// Going through categories list from front to back
|
||||||
//If category does not exist, list size <= category index
|
// If category does not exist, list size <= category index
|
||||||
//Thus, we can just add it here and not worry about indexing
|
// Thus, we can just add it here and not worry about indexing
|
||||||
newLocalCategories += this
|
newLocalCategories += this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(local.name != remote) {
|
if (local.name != remote) {
|
||||||
changed = true
|
changed = true
|
||||||
|
|
||||||
local.name = remote
|
local.name = remote
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Ensure consistent ordering
|
// Ensure consistent ordering
|
||||||
newLocalCategories.forEachIndexed { index, category ->
|
newLocalCategories.forEachIndexed { index, category ->
|
||||||
if(category.order != index) {
|
if (category.order != index) {
|
||||||
changed = true
|
changed = true
|
||||||
|
|
||||||
category.order = index
|
category.order = index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Only insert categories if changed
|
// Only insert categories if changed
|
||||||
if(changed)
|
if (changed)
|
||||||
db.insertCategories(newLocalCategories).executeAsBlocking()
|
db.insertCategories(newLocalCategories).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,10 +239,10 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
.build())
|
.build())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
if(!explicitlyRetryExhRequest(10, request)) {
|
if (!explicitlyRetryExhRequest(10, request)) {
|
||||||
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
|
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
|
||||||
|
|
||||||
if(prefs.eh_lenientSync().getOrDefault()) {
|
if (prefs.eh_lenientSync().getOrDefault()) {
|
||||||
errorList += errorString
|
errorList += errorString
|
||||||
} else {
|
} else {
|
||||||
status.onNext(FavoritesSyncStatus.Error(errorString))
|
status.onNext(FavoritesSyncStatus.Error(errorString))
|
||||||
@ -251,7 +254,7 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
private fun explicitlyRetryExhRequest(retryCount: Int, request: Request): Boolean {
|
private fun explicitlyRetryExhRequest(retryCount: Int, request: Request): Boolean {
|
||||||
var success = false
|
var success = false
|
||||||
|
|
||||||
for(i in 1 .. retryCount) {
|
for (i in 1..retryCount) {
|
||||||
try {
|
try {
|
||||||
val resp = exh.client.newCall(request).execute()
|
val resp = exh.client.newCall(request).execute()
|
||||||
|
|
||||||
@ -260,7 +263,7 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.w( "Sync network error!", e)
|
logger.w("Sync network error!", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,15 +271,15 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun applyChangeSetToRemote(errorList: MutableList<String>, changeSet: ChangeSet) {
|
private fun applyChangeSetToRemote(errorList: MutableList<String>, changeSet: ChangeSet) {
|
||||||
//Apply removals
|
// Apply removals
|
||||||
if(changeSet.removed.isNotEmpty()) {
|
if (changeSet.removed.isNotEmpty()) {
|
||||||
status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server"))
|
status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server"))
|
||||||
|
|
||||||
val formBody = FormBody.Builder()
|
val formBody = FormBody.Builder()
|
||||||
.add("ddact", "delete")
|
.add("ddact", "delete")
|
||||||
.add("apply", "Apply")
|
.add("apply", "Apply")
|
||||||
|
|
||||||
//Add change set to form
|
// Add change set to form
|
||||||
changeSet.removed.forEach {
|
changeSet.removed.forEach {
|
||||||
formBody.add("modifygids[]", it.gid)
|
formBody.add("modifygids[]", it.gid)
|
||||||
}
|
}
|
||||||
@ -286,10 +289,10 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
.post(formBody.build())
|
.post(formBody.build())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
if(!explicitlyRetryExhRequest(10, request)) {
|
if (!explicitlyRetryExhRequest(10, request)) {
|
||||||
val errorString = "Unable to delete galleries from the remote servers!"
|
val errorString = "Unable to delete galleries from the remote servers!"
|
||||||
|
|
||||||
if(prefs.eh_lenientSync().getOrDefault()) {
|
if (prefs.eh_lenientSync().getOrDefault()) {
|
||||||
errorList += errorString
|
errorList += errorString
|
||||||
} else {
|
} else {
|
||||||
status.onNext(FavoritesSyncStatus.Error(errorString))
|
status.onNext(FavoritesSyncStatus.Error(errorString))
|
||||||
@ -298,7 +301,7 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Apply additions
|
// Apply additions
|
||||||
throttleManager.resetThrottle()
|
throttleManager.resetThrottle()
|
||||||
changeSet.added.forEachIndexed { index, it ->
|
changeSet.added.forEachIndexed { index, it ->
|
||||||
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to remote server",
|
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to remote server",
|
||||||
@ -313,17 +316,17 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
private fun applyChangeSetToLocal(errorList: MutableList<String>, changeSet: ChangeSet) {
|
private fun applyChangeSetToLocal(errorList: MutableList<String>, changeSet: ChangeSet) {
|
||||||
val removedManga = mutableListOf<Manga>()
|
val removedManga = mutableListOf<Manga>()
|
||||||
|
|
||||||
//Apply removals
|
// Apply removals
|
||||||
changeSet.removed.forEachIndexed { index, it ->
|
changeSet.removed.forEachIndexed { index, it ->
|
||||||
status.onNext(FavoritesSyncStatus.Processing("Removing gallery ${index + 1} of ${changeSet.removed.size} from local library"))
|
status.onNext(FavoritesSyncStatus.Processing("Removing gallery ${index + 1} of ${changeSet.removed.size} from local library"))
|
||||||
val url = it.getUrl()
|
val url = it.getUrl()
|
||||||
|
|
||||||
//Consider both EX and EH sources
|
// Consider both EX and EH sources
|
||||||
listOf(db.getManga(url, EXH_SOURCE_ID),
|
listOf(db.getManga(url, EXH_SOURCE_ID),
|
||||||
db.getManga(url, EH_SOURCE_ID)).forEach {
|
db.getManga(url, EH_SOURCE_ID)).forEach {
|
||||||
val manga = it.executeAsBlocking()
|
val manga = it.executeAsBlocking()
|
||||||
|
|
||||||
if(manga?.favorite == true) {
|
if (manga?.favorite == true) {
|
||||||
manga.favorite = false
|
manga.favorite = false
|
||||||
db.updateMangaFavorite(manga).executeAsBlocking()
|
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||||
removedManga += manga
|
removedManga += manga
|
||||||
@ -339,7 +342,7 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
val insertedMangaCategories = mutableListOf<Pair<MangaCategory, Manga>>()
|
val insertedMangaCategories = mutableListOf<Pair<MangaCategory, Manga>>()
|
||||||
val categories = db.getCategories().executeAsBlocking()
|
val categories = db.getCategories().executeAsBlocking()
|
||||||
|
|
||||||
//Apply additions
|
// Apply additions
|
||||||
throttleManager.resetThrottle()
|
throttleManager.resetThrottle()
|
||||||
changeSet.added.forEachIndexed { index, it ->
|
changeSet.added.forEachIndexed { index, it ->
|
||||||
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to local library",
|
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to local library",
|
||||||
@ -347,14 +350,14 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
|
|
||||||
throttleManager.throttle()
|
throttleManager.throttle()
|
||||||
|
|
||||||
//Import using gallery adder
|
// Import using gallery adder
|
||||||
val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}",
|
val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}",
|
||||||
true,
|
true,
|
||||||
exh,
|
exh,
|
||||||
throttleManager::throttle)
|
throttleManager::throttle)
|
||||||
|
|
||||||
if(result is GalleryAddEvent.Fail) {
|
if (result is GalleryAddEvent.Fail) {
|
||||||
if(result is GalleryAddEvent.Fail.NotFound) {
|
if (result is GalleryAddEvent.Fail.NotFound) {
|
||||||
XLog.e("Remote gallery does not exist, skipping: %s!", it.getUrl())
|
XLog.e("Remote gallery does not exist, skipping: %s!", it.getUrl())
|
||||||
// Skip this gallery, it no longer exists
|
// Skip this gallery, it no longer exists
|
||||||
return@forEachIndexed
|
return@forEachIndexed
|
||||||
@ -365,13 +368,13 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
is GalleryAddEvent.Fail.UnknownType -> "'${it.title}' (${result.galleryUrl}) is not a valid gallery!"
|
is GalleryAddEvent.Fail.UnknownType -> "'${it.title}' (${result.galleryUrl}) is not a valid gallery!"
|
||||||
}
|
}
|
||||||
|
|
||||||
if(prefs.eh_lenientSync().getOrDefault()) {
|
if (prefs.eh_lenientSync().getOrDefault()) {
|
||||||
errorList += errorString
|
errorList += errorString
|
||||||
} else {
|
} else {
|
||||||
status.onNext(FavoritesSyncStatus.Error(errorString))
|
status.onNext(FavoritesSyncStatus.Error(errorString))
|
||||||
throw IgnoredException()
|
throw IgnoredException()
|
||||||
}
|
}
|
||||||
} else if(result is GalleryAddEvent.Success) {
|
} else if (result is GalleryAddEvent.Success) {
|
||||||
insertedMangaCategories += MangaCategory.create(result.manga,
|
insertedMangaCategories += MangaCategory.create(result.manga,
|
||||||
categories[it.category]) to result.manga
|
categories[it.category]) to result.manga
|
||||||
}
|
}
|
||||||
@ -385,8 +388,8 @@ class FavoritesSyncHelper(val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun needWarnThrottle()
|
fun needWarnThrottle() =
|
||||||
= throttleManager.throttleTime >= THROTTLE_WARN
|
throttleManager.throttleTime >= THROTTLE_WARN
|
||||||
|
|
||||||
class IgnoredException : RuntimeException()
|
class IgnoredException : RuntimeException()
|
||||||
|
|
||||||
@ -399,12 +402,14 @@ sealed class FavoritesSyncStatus(val message: String) {
|
|||||||
class Error(message: String) : FavoritesSyncStatus(message)
|
class Error(message: String) : FavoritesSyncStatus(message)
|
||||||
class Idle : FavoritesSyncStatus("Waiting for sync to start")
|
class Idle : FavoritesSyncStatus("Waiting for sync to start")
|
||||||
sealed class BadLibraryState(message: String) : FavoritesSyncStatus(message) {
|
sealed class BadLibraryState(message: String) : FavoritesSyncStatus(message) {
|
||||||
class MangaInMultipleCategories(val manga: Manga,
|
class MangaInMultipleCategories(
|
||||||
val categories: List<Category>):
|
val manga: Manga,
|
||||||
|
val categories: List<Category>
|
||||||
|
) :
|
||||||
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
|
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
|
||||||
}
|
}
|
||||||
class Initializing : FavoritesSyncStatus("Initializing sync")
|
class Initializing : FavoritesSyncStatus("Initializing sync")
|
||||||
class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(if(isThrottle)
|
class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(if (isThrottle)
|
||||||
"$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete."
|
"$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete."
|
||||||
else
|
else
|
||||||
message)
|
message)
|
||||||
|
@ -20,8 +20,8 @@ class LocalFavoritesStorage {
|
|||||||
|
|
||||||
fun getRealm() = Realm.getInstance(realmConfig)
|
fun getRealm() = Realm.getInstance(realmConfig)
|
||||||
|
|
||||||
fun getChangedDbEntries(realm: Realm)
|
fun getChangedDbEntries(realm: Realm) =
|
||||||
= getChangedEntries(realm,
|
getChangedEntries(realm,
|
||||||
parseToFavoriteEntries(
|
parseToFavoriteEntries(
|
||||||
loadDbCategories(
|
loadDbCategories(
|
||||||
db.getFavoriteMangas()
|
db.getFavoriteMangas()
|
||||||
@ -31,8 +31,8 @@ class LocalFavoritesStorage {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>)
|
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) =
|
||||||
= getChangedEntries(realm,
|
getChangedEntries(realm,
|
||||||
parseToFavoriteEntries(
|
parseToFavoriteEntries(
|
||||||
entries.asSequence().map {
|
entries.asSequence().map {
|
||||||
Pair(it.fav, it.manga.apply {
|
Pair(it.fav, it.manga.apply {
|
||||||
@ -51,10 +51,10 @@ class LocalFavoritesStorage {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
//Delete old snapshot
|
// Delete old snapshot
|
||||||
realm.delete(FavoriteEntry::class.java)
|
realm.delete(FavoriteEntry::class.java)
|
||||||
|
|
||||||
//Insert new snapshots
|
// Insert new snapshots
|
||||||
realm.copyToRealm(dbMangas.toList())
|
realm.copyToRealm(dbMangas.toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,18 +80,18 @@ class LocalFavoritesStorage {
|
|||||||
return ChangeSet(added, removed)
|
return ChangeSet(added, removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Realm.queryRealmForEntry(entry: FavoriteEntry)
|
private fun Realm.queryRealmForEntry(entry: FavoriteEntry) =
|
||||||
= where(FavoriteEntry::class.java)
|
where(FavoriteEntry::class.java)
|
||||||
.equalTo(FavoriteEntry::gid.name, entry.gid)
|
.equalTo(FavoriteEntry::gid.name, entry.gid)
|
||||||
.equalTo(FavoriteEntry::token.name, entry.token)
|
.equalTo(FavoriteEntry::token.name, entry.token)
|
||||||
.equalTo(FavoriteEntry::category.name, entry.category)
|
.equalTo(FavoriteEntry::category.name, entry.category)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
|
||||||
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry)
|
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) =
|
||||||
= list.find {
|
list.find {
|
||||||
it.gid == entry.gid
|
it.gid == entry.gid &&
|
||||||
&& it.token == entry.token
|
it.token == entry.token &&
|
||||||
&& it.category == entry.category
|
it.category == entry.category
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> {
|
private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> {
|
||||||
@ -105,8 +105,8 @@ class LocalFavoritesStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>)
|
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) =
|
||||||
= manga.filter {
|
manga.filter {
|
||||||
validateDbManga(it.second)
|
validateDbManga(it.second)
|
||||||
}.mapNotNull {
|
}.mapNotNull {
|
||||||
FavoriteEntry().apply {
|
FavoriteEntry().apply {
|
||||||
@ -115,18 +115,20 @@ class LocalFavoritesStorage {
|
|||||||
token = EHentaiSearchMetadata.galleryToken(it.second.url)
|
token = EHentaiSearchMetadata.galleryToken(it.second.url)
|
||||||
category = it.first
|
category = it.first
|
||||||
|
|
||||||
if(this.category > MAX_CATEGORIES)
|
if (this.category > MAX_CATEGORIES)
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateDbManga(manga: Manga)
|
private fun validateDbManga(manga: Manga) =
|
||||||
= manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
|
manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAX_CATEGORIES = 9
|
const val MAX_CATEGORIES = 9
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ChangeSet(val added: List<FavoriteEntry>,
|
data class ChangeSet(
|
||||||
val removed: List<FavoriteEntry>)
|
val added: List<FavoriteEntry>,
|
||||||
|
val removed: List<FavoriteEntry>
|
||||||
|
)
|
||||||
|
@ -4,42 +4,46 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.asObservable
|
import eu.kanade.tachiyomi.network.asObservable
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
|
import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
|
||||||
|
import java.security.MessageDigest
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.vepta.vdm.ByteCursor
|
import org.vepta.vdm.ByteCursor
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
private typealias HashedTerm = ByteArray
|
private typealias HashedTerm = ByteArray
|
||||||
|
|
||||||
private data class DataPair(val offset: Long, val length: Int)
|
private data class DataPair(val offset: Long, val length: Int)
|
||||||
private data class Node(val keys: List<ByteArray>,
|
private data class Node(
|
||||||
val datas: List<DataPair>,
|
val keys: List<ByteArray>,
|
||||||
val subnodeAddresses: List<Long>)
|
val datas: List<DataPair>,
|
||||||
|
val subnodeAddresses: List<Long>
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kotlin port of the hitomi.la search algorithm
|
* Kotlin port of the hitomi.la search algorithm
|
||||||
* @author NerdNumber9
|
* @author NerdNumber9
|
||||||
*/
|
*/
|
||||||
class HitomiNozomi(private val client: OkHttpClient,
|
class HitomiNozomi(
|
||||||
private val tagIndexVersion: Long,
|
private val client: OkHttpClient,
|
||||||
private val galleriesIndexVersion: Long) {
|
private val tagIndexVersion: Long,
|
||||||
|
private val galleriesIndexVersion: Long
|
||||||
|
) {
|
||||||
fun getGalleryIdsForQuery(query: String): Single<List<Int>> {
|
fun getGalleryIdsForQuery(query: String): Single<List<Int>> {
|
||||||
val replacedQuery = query.replace('_', ' ')
|
val replacedQuery = query.replace('_', ' ')
|
||||||
|
|
||||||
if(':' in replacedQuery) {
|
if (':' in replacedQuery) {
|
||||||
val sides = replacedQuery.split(':')
|
val sides = replacedQuery.split(':')
|
||||||
val namespace = sides[0]
|
val namespace = sides[0]
|
||||||
var tag = sides[1]
|
var tag = sides[1]
|
||||||
|
|
||||||
var area: String? = namespace
|
var area: String? = namespace
|
||||||
var language = "all"
|
var language = "all"
|
||||||
if(namespace == "female" || namespace == "male") {
|
if (namespace == "female" || namespace == "male") {
|
||||||
area = "tag"
|
area = "tag"
|
||||||
tag = replacedQuery
|
tag = replacedQuery
|
||||||
} else if(namespace == "language") {
|
} else if (namespace == "language") {
|
||||||
area = null
|
area = null
|
||||||
language = tag
|
language = tag
|
||||||
tag = "index"
|
tag = "index"
|
||||||
@ -52,7 +56,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
val field = "galleries"
|
val field = "galleries"
|
||||||
|
|
||||||
return getNodeAtAddress(field, 0).flatMap { node ->
|
return getNodeAtAddress(field, 0).flatMap { node ->
|
||||||
if(node == null) {
|
if (node == null) {
|
||||||
Single.just(null)
|
Single.just(null)
|
||||||
} else {
|
} else {
|
||||||
BSearch(field, key, node).flatMap { data ->
|
BSearch(field, key, node).flatMap { data ->
|
||||||
@ -67,12 +71,12 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
|
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
|
||||||
if(data == null)
|
if (data == null)
|
||||||
return Single.just(emptyList())
|
return Single.just(emptyList())
|
||||||
|
|
||||||
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
|
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
|
||||||
val (offset, length) = data
|
val (offset, length) = data
|
||||||
if(length > 100000000 || length <= 0)
|
if (length > 100000000 || length <= 0)
|
||||||
return Single.just(emptyList())
|
return Single.just(emptyList())
|
||||||
|
|
||||||
return client.newCall(rangedGet(url, offset, offset + length - 1))
|
return client.newCall(rangedGet(url, offset, offset + length - 1))
|
||||||
@ -82,7 +86,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
}
|
}
|
||||||
.onErrorReturn { ByteArray(0) }
|
.onErrorReturn { ByteArray(0) }
|
||||||
.map { inbuf ->
|
.map { inbuf ->
|
||||||
if(inbuf.isEmpty())
|
if (inbuf.isEmpty())
|
||||||
return@map emptyList<Int>()
|
return@map emptyList<Int>()
|
||||||
|
|
||||||
val view = ByteCursor(inbuf)
|
val view = ByteCursor(inbuf)
|
||||||
@ -90,13 +94,13 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
|
|
||||||
val expectedLength = numberOfGalleryIds * 4 + 4
|
val expectedLength = numberOfGalleryIds * 4 + 4
|
||||||
|
|
||||||
if(numberOfGalleryIds > 10000000
|
if (numberOfGalleryIds > 10000000 ||
|
||||||
|| numberOfGalleryIds <= 0
|
numberOfGalleryIds <= 0 ||
|
||||||
|| inbuf.size != expectedLength) {
|
inbuf.size != expectedLength) {
|
||||||
return@map emptyList<Int>()
|
return@map emptyList<Int>()
|
||||||
}
|
}
|
||||||
|
|
||||||
(1 .. numberOfGalleryIds).map {
|
(1..numberOfGalleryIds).map {
|
||||||
view.nextInt()
|
view.nextInt()
|
||||||
}
|
}
|
||||||
}.toSingle()
|
}.toSingle()
|
||||||
@ -105,12 +109,12 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
|
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
|
||||||
fun compareByteArrays(dv1: ByteArray, dv2: ByteArray): Int {
|
fun compareByteArrays(dv1: ByteArray, dv2: ByteArray): Int {
|
||||||
val top = Math.min(dv1.size, dv2.size)
|
val top = Math.min(dv1.size, dv2.size)
|
||||||
for(i in 0 until top) {
|
for (i in 0 until top) {
|
||||||
val dv1i = dv1[i].toInt() and 0xFF
|
val dv1i = dv1[i].toInt() and 0xFF
|
||||||
val dv2i = dv2[i].toInt() and 0xFF
|
val dv2i = dv2[i].toInt() and 0xFF
|
||||||
if(dv1i < dv2i)
|
if (dv1i < dv2i)
|
||||||
return -1
|
return -1
|
||||||
else if(dv1i > dv2i)
|
else if (dv1i > dv2i)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
@ -119,9 +123,9 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
fun locateKey(key: ByteArray, node: Node): Pair<Boolean, Int> {
|
fun locateKey(key: ByteArray, node: Node): Pair<Boolean, Int> {
|
||||||
var cmpResult = -1
|
var cmpResult = -1
|
||||||
var lastI = 0
|
var lastI = 0
|
||||||
for(nodeKey in node.keys) {
|
for (nodeKey in node.keys) {
|
||||||
cmpResult = compareByteArrays(key, nodeKey)
|
cmpResult = compareByteArrays(key, nodeKey)
|
||||||
if(cmpResult <= 0) break
|
if (cmpResult <= 0) break
|
||||||
lastI++
|
lastI++
|
||||||
}
|
}
|
||||||
return (cmpResult == 0) to lastI
|
return (cmpResult == 0) to lastI
|
||||||
@ -133,14 +137,14 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(node == null || node.keys.isEmpty()) {
|
if (node == null || node.keys.isEmpty()) {
|
||||||
return Single.just(null)
|
return Single.just(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val (there, where) = locateKey(key, node)
|
val (there, where) = locateKey(key, node)
|
||||||
if(there) {
|
if (there) {
|
||||||
return Single.just(node.datas[where])
|
return Single.just(node.datas[where])
|
||||||
} else if(isLeaf(node)) {
|
} else if (isLeaf(node)) {
|
||||||
return Single.just(null)
|
return Single.just(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,20 +158,20 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
|
|
||||||
val numberOfKeys = view.nextInt()
|
val numberOfKeys = view.nextInt()
|
||||||
|
|
||||||
val keys = (1 .. numberOfKeys).map {
|
val keys = (1..numberOfKeys).map {
|
||||||
val keySize = view.nextInt()
|
val keySize = view.nextInt()
|
||||||
view.next(keySize)
|
view.next(keySize)
|
||||||
}
|
}
|
||||||
|
|
||||||
val numberOfDatas = view.nextInt()
|
val numberOfDatas = view.nextInt()
|
||||||
val datas = (1 .. numberOfDatas).map {
|
val datas = (1..numberOfDatas).map {
|
||||||
val offset = view.nextLong()
|
val offset = view.nextLong()
|
||||||
val length = view.nextInt()
|
val length = view.nextInt()
|
||||||
DataPair(offset, length)
|
DataPair(offset, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
val numberOfSubnodeAddresses = B + 1
|
val numberOfSubnodeAddresses = B + 1
|
||||||
val subnodeAddresses = (1 .. numberOfSubnodeAddresses).map {
|
val subnodeAddresses = (1..numberOfSubnodeAddresses).map {
|
||||||
view.nextLong()
|
view.nextLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +180,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
|
|
||||||
private fun getNodeAtAddress(field: String, address: Long): Single<Node?> {
|
private fun getNodeAtAddress(field: String, address: Long): Single<Node?> {
|
||||||
var url = "$LTN_BASE_URL/$INDEX_DIR/$field.$tagIndexVersion.index"
|
var url = "$LTN_BASE_URL/$INDEX_DIR/$field.$tagIndexVersion.index"
|
||||||
if(field == "galleries") {
|
if (field == "galleries") {
|
||||||
url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.index"
|
url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.index"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +191,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
}
|
}
|
||||||
.onErrorReturn { ByteArray(0) }
|
.onErrorReturn { ByteArray(0) }
|
||||||
.map { nodedata ->
|
.map { nodedata ->
|
||||||
if(nodedata.isNotEmpty()) {
|
if (nodedata.isNotEmpty()) {
|
||||||
decodeNode(nodedata)
|
decodeNode(nodedata)
|
||||||
} else null
|
} else null
|
||||||
}.toSingle()
|
}.toSingle()
|
||||||
@ -195,7 +199,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
|
|
||||||
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> {
|
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> {
|
||||||
var nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$tag-$language$NOZOMI_EXTENSION"
|
var nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$tag-$language$NOZOMI_EXTENSION"
|
||||||
if(area != null) {
|
if (area != null) {
|
||||||
nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$tag-$language$NOZOMI_EXTENSION"
|
nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$tag-$language$NOZOMI_EXTENSION"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +210,7 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
.map { resp ->
|
.map { resp ->
|
||||||
val body = resp.body!!.bytes()
|
val body = resp.body!!.bytes()
|
||||||
val cursor = ByteCursor(body)
|
val cursor = ByteCursor(body)
|
||||||
(1 .. body.size / 4).map {
|
(1..body.size / 4).map {
|
||||||
cursor.nextInt()
|
cursor.nextInt()
|
||||||
}
|
}
|
||||||
}.toSingle()
|
}.toSingle()
|
||||||
@ -234,11 +238,10 @@ class HitomiNozomi(private val client: OkHttpClient,
|
|||||||
.build())
|
.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
|
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
|
||||||
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
|
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
.map { it.body!!.string().toLong() }
|
.map { it.body!!.string().toLong() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,17 +9,17 @@ class CrashlyticsPrinter(private val logLevel: Int) : Printer {
|
|||||||
* Print log in new line.
|
* Print log in new line.
|
||||||
*
|
*
|
||||||
* @param logLevel the level of log
|
* @param logLevel the level of log
|
||||||
* @param tag the tag of log
|
* @param tag the tag of log
|
||||||
* @param msg the msg of log
|
* @param msg the msg of log
|
||||||
*/
|
*/
|
||||||
override fun println(logLevel: Int, tag: String?, msg: String?) {
|
override fun println(logLevel: Int, tag: String?, msg: String?) {
|
||||||
if(logLevel >= this.logLevel) {
|
if (logLevel >= this.logLevel) {
|
||||||
try {
|
try {
|
||||||
Crashlytics.log(logLevel, tag, msg)
|
Crashlytics.log(logLevel, tag, msg)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
// Crash in debug if shit like this happens
|
// Crash in debug if shit like this happens
|
||||||
if(BuildConfig.DEBUG) throw t
|
if (BuildConfig.DEBUG) throw t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@ import android.content.Context
|
|||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import com.ms_square.debugoverlay.DataObserver
|
import com.ms_square.debugoverlay.DataObserver
|
||||||
import com.ms_square.debugoverlay.OverlayModule
|
import com.ms_square.debugoverlay.OverlayModule
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
@ -61,5 +61,5 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
|
|||||||
<b>Source blacklist:</b> ${prefs.eh_enableSourceBlacklist().getOrDefault().asEnabledString()}
|
<b>Source blacklist:</b> ${prefs.eh_enableSourceBlacklist().getOrDefault().asEnabledString()}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
private fun Boolean.asEnabledString() = if(this) "enabled" else "disabled"
|
private fun Boolean.asEnabledString() = if (this) "enabled" else "disabled"
|
||||||
}
|
}
|
||||||
|
@ -23,4 +23,4 @@ enum class EHLogLevel(val description: String) {
|
|||||||
return curLogLevel!! >= requiredLogLevel.ordinal
|
return curLogLevel!! >= requiredLogLevel.ordinal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package exh.log
|
|||||||
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
fun OkHttpClient.Builder.maybeInjectEHLogger(): OkHttpClient.Builder { //TODO - un-break this
|
fun OkHttpClient.Builder.maybeInjectEHLogger(): OkHttpClient.Builder { // TODO - un-break this
|
||||||
/* if(false &&EHLogLevel.shouldLog(EHLogLevel.EXTREME)) {
|
/* if(false &&EHLogLevel.shouldLog(EHLogLevel.EXTREME)) {
|
||||||
val xLogger = XLog.tag("EHNetwork")
|
val xLogger = XLog.tag("EHNetwork")
|
||||||
.nst()
|
.nst()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package exh.metadata
|
package exh.metadata
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata utils
|
* Metadata utils
|
||||||
@ -35,13 +35,12 @@ fun parseHumanReadableByteCount(arg0: String): Double? {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String?.nullIfBlank(): String? = if (isNullOrBlank())
|
||||||
fun String?.nullIfBlank(): String? = if(isNullOrBlank())
|
|
||||||
null
|
null
|
||||||
else
|
else
|
||||||
this
|
this
|
||||||
|
|
||||||
fun <K,V> Set<Map.Entry<K,V>>.forEach(action: (K, V) -> Unit) {
|
fun <K, V> Set<Map.Entry<K, V>>.forEach(action: (K, V) -> Unit) {
|
||||||
forEach { action(it.key, it.value) }
|
forEach { action(it.key, it.value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,12 +4,14 @@ import android.net.Uri
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import exh.metadata.*
|
import exh.metadata.EX_DATE_FORMAT
|
||||||
|
import exh.metadata.ONGOING_SUFFIX
|
||||||
|
import exh.metadata.humanReadableByteCount
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import exh.plusAssign
|
import exh.plusAssign
|
||||||
|
import java.util.Date
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||||
var gId: String?
|
var gId: String?
|
||||||
@ -27,7 +29,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
|
|
||||||
var datePosted: Long? = null
|
var datePosted: Long? = null
|
||||||
var parent: String? = null
|
var parent: String? = null
|
||||||
var visible: String? = null //Not a boolean
|
var visible: String? = null // Not a boolean
|
||||||
var language: String? = null
|
var language: String? = null
|
||||||
var translated: Boolean? = null
|
var translated: Boolean? = null
|
||||||
var size: Long? = null
|
var size: Long? = null
|
||||||
@ -47,23 +49,23 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
}
|
}
|
||||||
thumbnailUrl?.let { manga.thumbnail_url = it }
|
thumbnailUrl?.let { manga.thumbnail_url = it }
|
||||||
|
|
||||||
//No title bug?
|
// No title bug?
|
||||||
val titleObj = if(Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault())
|
val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault())
|
||||||
altTitle ?: title
|
altTitle ?: title
|
||||||
else
|
else
|
||||||
title
|
title
|
||||||
titleObj?.let { manga.title = it }
|
titleObj?.let { manga.title = it }
|
||||||
|
|
||||||
//Set artist (if we can find one)
|
// Set artist (if we can find one)
|
||||||
tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let {
|
tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let {
|
||||||
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
|
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
//Copy tags -> genres
|
// Copy tags -> genres
|
||||||
manga.genre = tagsToGenreString()
|
manga.genre = tagsToGenreString()
|
||||||
|
|
||||||
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||||
//We default to completed
|
// We default to completed
|
||||||
manga.status = SManga.COMPLETED
|
manga.status = SManga.COMPLETED
|
||||||
title?.let { t ->
|
title?.let { t ->
|
||||||
ONGOING_SUFFIX.find {
|
ONGOING_SUFFIX.find {
|
||||||
@ -73,7 +75,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Build a nice looking description out of what we know
|
// Build a nice looking description out of what we know
|
||||||
val titleDesc = StringBuilder()
|
val titleDesc = StringBuilder()
|
||||||
title?.let { titleDesc += "Title: $it\n" }
|
title?.let { titleDesc += "Title: $it\n" }
|
||||||
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
|
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
|
||||||
@ -85,7 +87,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
visible?.let { detailsDesc += "Visible: $it\n" }
|
visible?.let { detailsDesc += "Visible: $it\n" }
|
||||||
language?.let {
|
language?.let {
|
||||||
detailsDesc += "Language: $it"
|
detailsDesc += "Language: $it"
|
||||||
if(translated == true) detailsDesc += " TR"
|
if (translated == true) detailsDesc += " TR"
|
||||||
detailsDesc += "\n"
|
detailsDesc += "\n"
|
||||||
}
|
}
|
||||||
size?.let { detailsDesc += "File size: ${humanReadableByteCount(it, true)}\n" }
|
size?.let { detailsDesc += "File size: ${humanReadableByteCount(it, true)}\n" }
|
||||||
@ -114,10 +116,10 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
const val EH_GENRE_NAMESPACE = "genre"
|
const val EH_GENRE_NAMESPACE = "genre"
|
||||||
private const val EH_ARTIST_NAMESPACE = "artist"
|
private const val EH_ARTIST_NAMESPACE = "artist"
|
||||||
|
|
||||||
private fun splitGalleryUrl(url: String)
|
private fun splitGalleryUrl(url: String) =
|
||||||
= url.let {
|
url.let {
|
||||||
//Only parse URL if is full URL
|
// Only parse URL if is full URL
|
||||||
val pathSegments = if(it.startsWith("http"))
|
val pathSegments = if (it.startsWith("http"))
|
||||||
Uri.parse(it).pathSegments
|
Uri.parse(it).pathSegments
|
||||||
else
|
else
|
||||||
it.split('/')
|
it.split('/')
|
||||||
@ -129,10 +131,10 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
fun galleryToken(url: String) =
|
fun galleryToken(url: String) =
|
||||||
splitGalleryUrl(url)[2]
|
splitGalleryUrl(url)[2]
|
||||||
|
|
||||||
fun normalizeUrl(url: String)
|
fun normalizeUrl(url: String) =
|
||||||
= idAndTokenToUrl(galleryId(url), galleryToken(url))
|
idAndTokenToUrl(galleryId(url), galleryToken(url))
|
||||||
|
|
||||||
fun idAndTokenToUrl(id: String, token: String)
|
fun idAndTokenToUrl(id: String, token: String) =
|
||||||
= "/g/$id/$token/?nw=always"
|
"/g/$id/$token/?nw=always"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,6 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
|
|||||||
manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
|
manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
|
||||||
.filter(String::isNotBlank)
|
.filter(String::isNotBlank)
|
||||||
.joinToString(separator = "\n")
|
.joinToString(separator = "\n")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -47,4 +46,4 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
|
|||||||
const val TAGS_NAMESPACE = "tags"
|
const val TAGS_NAMESPACE = "tags"
|
||||||
const val ARTIST_NAMESPACE = "artist"
|
const val ARTIST_NAMESPACE = "artist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Guess thumbnail URL if manga does not have thumbnail URL
|
// Guess thumbnail URL if manga does not have thumbnail URL
|
||||||
if(manga.thumbnail_url.isNullOrBlank()) {
|
if (manga.thumbnail_url.isNullOrBlank()) {
|
||||||
manga.thumbnail_url = guessThumbnailUrl(hbId.toString())
|
manga.thumbnail_url = guessThumbnailUrl(hbId.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,4 +49,4 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
|
|||||||
return "$BASE_URL/thumbnails/${hbid}_1.jpg#guessed"
|
return "$BASE_URL/thumbnails/${hbid}_1.jpg#guessed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
|
|||||||
manga.artist = artist
|
manga.artist = artist
|
||||||
manga.author = artist
|
manga.author = artist
|
||||||
|
|
||||||
//Not available
|
// Not available
|
||||||
manga.status = SManga.UNKNOWN
|
manga.status = SManga.UNKNOWN
|
||||||
|
|
||||||
val detailsDesc = "Title: $title\n" +
|
val detailsDesc = "Title: $title\n" +
|
||||||
@ -49,7 +49,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
|
|||||||
|
|
||||||
const val BASE_URL = "https://hentai.cafe"
|
const val BASE_URL = "https://hentai.cafe"
|
||||||
|
|
||||||
fun hcIdFromUrl(url: String)
|
fun hcIdFromUrl(url: String) =
|
||||||
= url.split("/").last { it.isNotBlank() }
|
url.split("/").last { it.isNotBlank() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import exh.metadata.EX_DATE_FORMAT
|
import exh.metadata.EX_DATE_FORMAT
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import exh.plusAssign
|
import exh.plusAssign
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
class HitomiSearchMetadata: RaisedSearchMetadata() {
|
class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||||
var url get() = hlId?.let { urlFromHlId(it) }
|
var url get() = hlId?.let { urlFromHlId(it) }
|
||||||
set(a) {
|
set(a) {
|
||||||
a?.let {
|
a?.let {
|
||||||
@ -62,10 +62,10 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
|
|||||||
detailsDesc += "Language: ${it.capitalize()}\n"
|
detailsDesc += "Language: ${it.capitalize()}\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
if(series.isNotEmpty())
|
if (series.isNotEmpty())
|
||||||
detailsDesc += "Series: ${series.joinToString()}\n"
|
detailsDesc += "Series: ${series.joinToString()}\n"
|
||||||
|
|
||||||
if(characters.isNotEmpty())
|
if (characters.isNotEmpty())
|
||||||
detailsDesc += "Characters: ${characters.joinToString()}\n"
|
detailsDesc += "Characters: ${characters.joinToString()}\n"
|
||||||
|
|
||||||
uploadDate?.let {
|
uploadDate?.let {
|
||||||
@ -74,7 +74,7 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
|
|||||||
|
|
||||||
manga.status = SManga.UNKNOWN
|
manga.status = SManga.UNKNOWN
|
||||||
|
|
||||||
//Copy tags -> genres
|
// Copy tags -> genres
|
||||||
manga.genre = tagsToGenreString()
|
manga.genre = tagsToGenreString()
|
||||||
|
|
||||||
val tagsDesc = tagsToDescription()
|
val tagsDesc = tagsToDescription()
|
||||||
@ -92,10 +92,10 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
|
|||||||
const val LTN_BASE_URL = "https://ltn.hitomi.la"
|
const val LTN_BASE_URL = "https://ltn.hitomi.la"
|
||||||
const val BASE_URL = "https://hitomi.la"
|
const val BASE_URL = "https://hitomi.la"
|
||||||
|
|
||||||
fun hlIdFromUrl(url: String)
|
fun hlIdFromUrl(url: String) =
|
||||||
= url.split('/').last().split('-').last().substringBeforeLast('.')
|
url.split('/').last().split('-').last().substringBeforeLast('.')
|
||||||
|
|
||||||
fun urlFromHlId(id: String)
|
fun urlFromHlId(id: String) =
|
||||||
= "$BASE_URL/galleries/$id.html"
|
"$BASE_URL/galleries/$id.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,14 @@ package exh.metadata.metadata
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import exh.metadata.*
|
import exh.metadata.EX_DATE_FORMAT
|
||||||
|
import exh.metadata.ONGOING_SUFFIX
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
|
import exh.metadata.nullIfBlank
|
||||||
import exh.plusAssign
|
import exh.plusAssign
|
||||||
|
import java.util.Date
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||||
var url get() = nhId?.let { BASE_URL + nhIdToPath(it) }
|
var url get() = nhId?.let { BASE_URL + nhIdToPath(it) }
|
||||||
@ -39,10 +41,10 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
override fun copyTo(manga: SManga) {
|
override fun copyTo(manga: SManga) {
|
||||||
nhId?.let { manga.url = nhIdToPath(it) }
|
nhId?.let { manga.url = nhIdToPath(it) }
|
||||||
|
|
||||||
if(mediaId != null) {
|
if (mediaId != null) {
|
||||||
val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault()
|
val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault()
|
||||||
typeToExtension(if(hqThumbs) coverImageType else thumbnailImageType)?.let {
|
typeToExtension(if (hqThumbs) coverImageType else thumbnailImageType)?.let {
|
||||||
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if(hqThumbs)
|
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if (hqThumbs)
|
||||||
"cover"
|
"cover"
|
||||||
else "thumb"}.$it"
|
else "thumb"}.$it"
|
||||||
}
|
}
|
||||||
@ -50,21 +52,21 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
|
|
||||||
manga.title = englishTitle ?: japaneseTitle ?: shortTitle!!
|
manga.title = englishTitle ?: japaneseTitle ?: shortTitle!!
|
||||||
|
|
||||||
//Set artist (if we can find one)
|
// Set artist (if we can find one)
|
||||||
tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let {
|
tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let {
|
||||||
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
|
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
var category: String? = null
|
var category: String? = null
|
||||||
tags.filter { it.namespace == NHENTAI_CATEGORIES_NAMESPACE }.let {
|
tags.filter { it.namespace == NHENTAI_CATEGORIES_NAMESPACE }.let {
|
||||||
if(it.isNotEmpty()) category = it.joinToString(transform = { it.name })
|
if (it.isNotEmpty()) category = it.joinToString(transform = { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
//Copy tags -> genres
|
// Copy tags -> genres
|
||||||
manga.genre = tagsToGenreString()
|
manga.genre = tagsToGenreString()
|
||||||
|
|
||||||
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||||
//We default to completed
|
// We default to completed
|
||||||
manga.status = SManga.COMPLETED
|
manga.status = SManga.COMPLETED
|
||||||
englishTitle?.let { t ->
|
englishTitle?.let { t ->
|
||||||
ONGOING_SUFFIX.find {
|
ONGOING_SUFFIX.find {
|
||||||
@ -106,14 +108,14 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
|||||||
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
|
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
|
||||||
|
|
||||||
fun typeToExtension(t: String?) =
|
fun typeToExtension(t: String?) =
|
||||||
when(t) {
|
when (t) {
|
||||||
"p" -> "png"
|
"p" -> "png"
|
||||||
"j" -> "jpg"
|
"j" -> "jpg"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nhUrlToId(url: String)
|
fun nhUrlToId(url: String) =
|
||||||
= url.split("/").last { it.isNotBlank() }.toLong()
|
url.split("/").last { it.isNotBlank() }.toLong()
|
||||||
|
|
||||||
fun nhIdToPath(id: Long) = "/g/$id/"
|
fun nhIdToPath(id: Long) = "/g/$id/"
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
|||||||
manga.title = it
|
manga.title = it
|
||||||
titleDesc += "Title: $it\n"
|
titleDesc += "Title: $it\n"
|
||||||
}
|
}
|
||||||
if(altTitles.isNotEmpty())
|
if (altTitles.isNotEmpty())
|
||||||
titleDesc += "Alternate Titles: \n" + altTitles
|
titleDesc += "Alternate Titles: \n" + altTitles
|
||||||
.joinToString(separator = "\n", postfix = "\n") {
|
.joinToString(separator = "\n", postfix = "\n") {
|
||||||
"▪ $it"
|
"▪ $it"
|
||||||
@ -58,7 +58,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
status?.let {
|
status?.let {
|
||||||
manga.status = when(it) {
|
manga.status = when (it) {
|
||||||
"Ongoing" -> SManga.ONGOING
|
"Ongoing" -> SManga.ONGOING
|
||||||
"Completed", "Suspended" -> SManga.COMPLETED
|
"Completed", "Suspended" -> SManga.COMPLETED
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
@ -70,7 +70,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
|||||||
detailsDesc += "Rating: %.2\n".format(it)
|
detailsDesc += "Rating: %.2\n".format(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Copy tags -> genres
|
// Copy tags -> genres
|
||||||
manga.genre = tagsToGenreString()
|
manga.genre = tagsToGenreString()
|
||||||
|
|
||||||
val tagsDesc = tagsToDescription()
|
val tagsDesc = tagsToDescription()
|
||||||
@ -80,15 +80,14 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
|||||||
.joinToString(separator = "\n")
|
.joinToString(separator = "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TITLE_TYPE_MAIN = 0
|
private const val TITLE_TYPE_MAIN = 0
|
||||||
private const val TITLE_TYPE_ALT = 1
|
private const val TITLE_TYPE_ALT = 1
|
||||||
|
|
||||||
const val TAG_TYPE_DEFAULT = 0
|
const val TAG_TYPE_DEFAULT = 0
|
||||||
|
|
||||||
private fun splitGalleryUrl(url: String)
|
private fun splitGalleryUrl(url: String) =
|
||||||
= url.let {
|
url.let {
|
||||||
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
|
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,13 +96,13 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class PervEdenLang(val id: Long) {
|
enum class PervEdenLang(val id: Long) {
|
||||||
//DO NOT RENAME THESE TO CAPITAL LETTERS! The enum names are used to build URLs
|
// DO NOT RENAME THESE TO CAPITAL LETTERS! The enum names are used to build URLs
|
||||||
en(PERV_EDEN_EN_SOURCE_ID),
|
en(PERV_EDEN_EN_SOURCE_ID),
|
||||||
it(PERV_EDEN_IT_SOURCE_ID);
|
it(PERV_EDEN_IT_SOURCE_ID);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun source(id: Long)
|
fun source(id: Long) =
|
||||||
= values().find { it.id == id }
|
values().find { it.id == id }
|
||||||
?: throw IllegalArgumentException("Unknown source ID: $id!")
|
?: throw IllegalArgumentException("Unknown source ID: $id!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
|
|||||||
altTitle?.let { titleDesc += "Japanese Title: $it\n" }
|
altTitle?.let { titleDesc += "Japanese Title: $it\n" }
|
||||||
|
|
||||||
val detailsDesc = StringBuilder()
|
val detailsDesc = StringBuilder()
|
||||||
(uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n"}
|
(uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n" }
|
||||||
pages?.let { detailsDesc += "Length: $it pages\n" }
|
pages?.let { detailsDesc += "Length: $it pages\n" }
|
||||||
fileSize?.let { detailsDesc += "Size: $it\n" }
|
fileSize?.let { detailsDesc += "Size: $it\n" }
|
||||||
ratingCount?.let { detailsDesc += "Rating: $averageRating ($ratingCount)\n" }
|
ratingCount?.let { detailsDesc += "Rating: $averageRating ($ratingCount)\n" }
|
||||||
@ -69,4 +69,4 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
|
|||||||
|
|
||||||
val BASE_URL = "https://pururin.io"
|
val BASE_URL = "https://pururin.io"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import exh.metadata.EX_DATE_FORMAT
|
import exh.metadata.EX_DATE_FORMAT
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import exh.plusAssign
|
import exh.plusAssign
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||||
var tmId: Int? = null
|
var tmId: Int? = null
|
||||||
@ -51,15 +51,15 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
|||||||
collection?.let { detailsDesc += "Collection: $it\n" }
|
collection?.let { detailsDesc += "Collection: $it\n" }
|
||||||
group?.let { detailsDesc += "Group: $it\n" }
|
group?.let { detailsDesc += "Group: $it\n" }
|
||||||
val parodiesString = parody.joinToString()
|
val parodiesString = parody.joinToString()
|
||||||
if(parodiesString.isNotEmpty()) {
|
if (parodiesString.isNotEmpty()) {
|
||||||
detailsDesc += "Parody: $parodiesString\n"
|
detailsDesc += "Parody: $parodiesString\n"
|
||||||
}
|
}
|
||||||
val charactersString = character.joinToString()
|
val charactersString = character.joinToString()
|
||||||
if(charactersString.isNotEmpty()) {
|
if (charactersString.isNotEmpty()) {
|
||||||
detailsDesc += "Character: $charactersString\n"
|
detailsDesc += "Character: $charactersString\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
//Copy tags -> genres
|
// Copy tags -> genres
|
||||||
manga.genre = tagsToGenreString()
|
manga.genre = tagsToGenreString()
|
||||||
|
|
||||||
val tagsDesc = tagsToDescription()
|
val tagsDesc = tagsToDescription()
|
||||||
@ -76,8 +76,8 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
|||||||
|
|
||||||
val BASE_URL = "https://www.tsumino.com"
|
val BASE_URL = "https://www.tsumino.com"
|
||||||
|
|
||||||
fun tmIdFromUrl(url: String)
|
fun tmIdFromUrl(url: String) =
|
||||||
= Uri.parse(url).lastPathSegment
|
Uri.parse(url).lastPathSegment
|
||||||
|
|
||||||
fun mangaUrlFromId(id: String) = "/Book/Info/$id"
|
fun mangaUrlFromId(id: String) = "/Book/Info/$id"
|
||||||
|
|
||||||
|
@ -5,19 +5,19 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||||||
import exh.metadata.sql.models.SearchMetadata
|
import exh.metadata.sql.models.SearchMetadata
|
||||||
import exh.metadata.sql.models.SearchTag
|
import exh.metadata.sql.models.SearchTag
|
||||||
import exh.metadata.sql.models.SearchTitle
|
import exh.metadata.sql.models.SearchTitle
|
||||||
|
import kotlin.reflect.KClass
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Single
|
import rx.Single
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
data class FlatMetadata(
|
data class FlatMetadata(
|
||||||
val metadata: SearchMetadata,
|
val metadata: SearchMetadata,
|
||||||
val tags: List<SearchTag>,
|
val tags: List<SearchTag>,
|
||||||
val titles: List<SearchTitle>
|
val titles: List<SearchTitle>
|
||||||
) {
|
) {
|
||||||
inline fun <reified T : RaisedSearchMetadata> raise(): T = raise(T::class)
|
inline fun <reified T : RaisedSearchMetadata> raise(): T = raise(T::class)
|
||||||
|
|
||||||
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>)
|
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>) =
|
||||||
= RaisedSearchMetadata.raiseFlattenGson
|
RaisedSearchMetadata.raiseFlattenGson
|
||||||
.fromJson(metadata.extra, clazz.java).apply {
|
.fromJson(metadata.extra, clazz.java).apply {
|
||||||
fillBaseFields(this@FlatMetadata)
|
fillBaseFields(this@FlatMetadata)
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedOperation<Fla
|
|||||||
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
|
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
|
||||||
val single = Single.fromCallable {
|
val single = Single.fromCallable {
|
||||||
val meta = getSearchMetadataForManga(mangaId).executeAsBlocking()
|
val meta = getSearchMetadataForManga(mangaId).executeAsBlocking()
|
||||||
if(meta != null) {
|
if (meta != null) {
|
||||||
val tags = getSearchTagsForManga(mangaId).executeAsBlocking()
|
val tags = getSearchTagsForManga(mangaId).executeAsBlocking()
|
||||||
val titles = getSearchTitlesForManga(mangaId).executeAsBlocking()
|
val titles = getSearchTitlesForManga(mangaId).executeAsBlocking()
|
||||||
|
|
||||||
@ -92,4 +92,4 @@ fun DatabaseHelper.insertFlatMetadata(flatMetadata: FlatMetadata) = Completable.
|
|||||||
setSearchTagsForManga(flatMetadata.metadata.mangaId, flatMetadata.tags)
|
setSearchTagsForManga(flatMetadata.metadata.mangaId, flatMetadata.tags)
|
||||||
setSearchTitlesForManga(flatMetadata.metadata.mangaId, flatMetadata.titles)
|
setSearchTitlesForManga(flatMetadata.metadata.mangaId, flatMetadata.titles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,18 +30,18 @@ abstract class RaisedSearchMetadata {
|
|||||||
|
|
||||||
fun replaceTitleOfType(type: Int, newTitle: String?) {
|
fun replaceTitleOfType(type: Int, newTitle: String?) {
|
||||||
titles.removeAll { it.type == type }
|
titles.removeAll { it.type == type }
|
||||||
if(newTitle != null) titles += RaisedTitle(newTitle, type)
|
if (newTitle != null) titles += RaisedTitle(newTitle, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun copyTo(manga: SManga)
|
abstract fun copyTo(manga: SManga)
|
||||||
|
|
||||||
fun tagsToGenreString()
|
fun tagsToGenreString() =
|
||||||
= tags.filter { it.type != TAG_TYPE_VIRTUAL }
|
tags.filter { it.type != TAG_TYPE_VIRTUAL }
|
||||||
.joinToString { (if(it.namespace != null) "${it.namespace}: " else "") + it.name }
|
.joinToString { (if (it.namespace != null) "${it.namespace}: " else "") + it.name }
|
||||||
|
|
||||||
fun tagsToDescription()
|
fun tagsToDescription() =
|
||||||
= StringBuilder("Tags:\n").apply {
|
StringBuilder("Tags:\n").apply {
|
||||||
//BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
// BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
||||||
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
|
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
|
||||||
it.namespace
|
it.namespace
|
||||||
}.entries
|
}.entries
|
||||||
@ -49,7 +49,7 @@ abstract class RaisedSearchMetadata {
|
|||||||
groupedTags.forEach { namespace, tags ->
|
groupedTags.forEach { namespace, tags ->
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
||||||
if(namespace != null) {
|
if (namespace != null) {
|
||||||
this += "▪ "
|
this += "▪ "
|
||||||
this += namespace
|
this += namespace
|
||||||
this += ": "
|
this += ": "
|
||||||
@ -125,8 +125,8 @@ abstract class RaisedSearchMetadata {
|
|||||||
* @param property the metadata for the property.
|
* @param property the metadata for the property.
|
||||||
* @return the property value.
|
* @return the property value.
|
||||||
*/
|
*/
|
||||||
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>)
|
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) =
|
||||||
= thisRef.getTitleOfType(type)
|
thisRef.getTitleOfType(type)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the value of the property for the given object.
|
* Sets the value of the property for the given object.
|
||||||
@ -134,8 +134,8 @@ abstract class RaisedSearchMetadata {
|
|||||||
* @param property the metadata for the property.
|
* @param property the metadata for the property.
|
||||||
* @param value the value to set.
|
* @param value the value to set.
|
||||||
*/
|
*/
|
||||||
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?)
|
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) =
|
||||||
= thisRef.replaceTitleOfType(type, value)
|
thisRef.replaceTitleOfType(type, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package exh.metadata.metadata.base
|
package exh.metadata.metadata.base
|
||||||
|
|
||||||
data class RaisedTag(val namespace: String?,
|
data class RaisedTag(
|
||||||
val name: String,
|
val namespace: String?,
|
||||||
val type: Int)
|
val name: String,
|
||||||
|
val type: Int
|
||||||
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package exh.metadata.metadata.base
|
package exh.metadata.metadata.base
|
||||||
|
|
||||||
data class RaisedTitle(
|
data class RaisedTitle(
|
||||||
val title: String,
|
val title: String,
|
||||||
val type: Int = 0
|
val type: Int = 0
|
||||||
)
|
)
|
||||||
|
@ -2,20 +2,20 @@ package exh.metadata.sql.models
|
|||||||
|
|
||||||
data class SearchMetadata(
|
data class SearchMetadata(
|
||||||
// Manga ID this gallery is linked to
|
// Manga ID this gallery is linked to
|
||||||
val mangaId: Long,
|
val mangaId: Long,
|
||||||
|
|
||||||
// Gallery uploader
|
// Gallery uploader
|
||||||
val uploader: String?,
|
val uploader: String?,
|
||||||
|
|
||||||
// Extra data attached to this metadata, in JSON format
|
// Extra data attached to this metadata, in JSON format
|
||||||
val extra: String,
|
val extra: String,
|
||||||
|
|
||||||
// Indexed extra data attached to this metadata
|
// Indexed extra data attached to this metadata
|
||||||
val indexedExtra: String?,
|
val indexedExtra: String?,
|
||||||
|
|
||||||
// The version of this metadata's extra. Used to track changes to the 'extra' field's schema
|
// The version of this metadata's extra. Used to track changes to the 'extra' field's schema
|
||||||
val extraVersion: Int
|
val extraVersion: Int
|
||||||
) {
|
) {
|
||||||
// Transient information attached to this piece of metadata, useful for caching
|
// Transient information attached to this piece of metadata, useful for caching
|
||||||
var transientCache: Map<String, Any>? = null
|
var transientCache: Map<String, Any>? = null
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,17 @@ package exh.metadata.sql.models
|
|||||||
|
|
||||||
data class SearchTag(
|
data class SearchTag(
|
||||||
// Tag identifier, unique
|
// Tag identifier, unique
|
||||||
val id: Long?,
|
val id: Long?,
|
||||||
|
|
||||||
// Metadata this tag is attached to
|
// Metadata this tag is attached to
|
||||||
val mangaId: Long,
|
val mangaId: Long,
|
||||||
|
|
||||||
// Tag namespace
|
// Tag namespace
|
||||||
val namespace: String?,
|
val namespace: String?,
|
||||||
|
|
||||||
// Tag name
|
// Tag name
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
||||||
// Tag type
|
// Tag type
|
||||||
val type: Int
|
val type: Int
|
||||||
)
|
)
|
||||||
|
@ -2,14 +2,14 @@ package exh.metadata.sql.models
|
|||||||
|
|
||||||
data class SearchTitle(
|
data class SearchTitle(
|
||||||
// Title identifier, unique
|
// Title identifier, unique
|
||||||
val id: Long?,
|
val id: Long?,
|
||||||
|
|
||||||
// Metadata this title is attached to
|
// Metadata this title is attached to
|
||||||
val mangaId: Long,
|
val mangaId: Long,
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
val title: String,
|
val title: String,
|
||||||
|
|
||||||
// Title type, useful for distinguishing between main/alt titles
|
// Title type, useful for distinguishing between main/alt titles
|
||||||
val type: Int
|
val type: Int
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,6 @@ package exh.metadata.sql.queries
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import exh.metadata.sql.models.SearchMetadata
|
import exh.metadata.sql.models.SearchMetadata
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
|
|
||||||
@ -42,4 +41,4 @@ interface SearchMetadataQueries : DbProvider {
|
|||||||
.table(SearchMetadataTable.TABLE)
|
.table(SearchMetadataTable.TABLE)
|
||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -44,4 +44,4 @@ interface SearchTagQueries : DbProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,6 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.inTransaction
|
import eu.kanade.tachiyomi.data.database.inTransaction
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import exh.metadata.sql.models.SearchMetadata
|
|
||||||
import exh.metadata.sql.models.SearchTitle
|
import exh.metadata.sql.models.SearchTitle
|
||||||
import exh.metadata.sql.tables.SearchTitleTable
|
import exh.metadata.sql.tables.SearchTitleTable
|
||||||
|
|
||||||
@ -46,4 +44,4 @@ interface SearchTitleQueries : DbProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
|
val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
|
||||||
if(!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
response.interceptAsHtml { doc ->
|
response.interceptAsHtml { doc ->
|
||||||
// Find captcha
|
// Find captcha
|
||||||
if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) {
|
if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) {
|
||||||
@ -20,4 +20,4 @@ val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else response
|
} else response
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
package exh.search
|
package exh.search
|
||||||
|
|
||||||
class Namespace(var namespace: String,
|
class Namespace(
|
||||||
var tag: Text? = null) : QueryComponent()
|
var namespace: String,
|
||||||
|
var tag: Text? = null
|
||||||
|
) : QueryComponent()
|
||||||
|
@ -3,4 +3,4 @@ package exh.search
|
|||||||
open class QueryComponent {
|
open class QueryComponent {
|
||||||
var excluded = false
|
var excluded = false
|
||||||
var exact = false
|
var exact = false
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,10 @@ import exh.metadata.sql.tables.SearchTitleTable
|
|||||||
class SearchEngine {
|
class SearchEngine {
|
||||||
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
|
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
|
||||||
|
|
||||||
fun textToSubQueries(namespace: String?,
|
fun textToSubQueries(
|
||||||
component: Text?): Pair<String, List<String>>? {
|
namespace: String?,
|
||||||
|
component: Text?
|
||||||
|
): Pair<String, List<String>>? {
|
||||||
val maybeLenientComponent = component?.let {
|
val maybeLenientComponent = component?.let {
|
||||||
if (!it.exact)
|
if (!it.exact)
|
||||||
it.asLenientTagQueries()
|
it.asLenientTagQueries()
|
||||||
@ -22,20 +24,20 @@ class SearchEngine {
|
|||||||
"${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?"
|
"${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?"
|
||||||
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params
|
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params
|
||||||
}
|
}
|
||||||
return if(namespace != null) {
|
return if (namespace != null) {
|
||||||
var query = """
|
var query = """
|
||||||
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
||||||
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
|
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
|
||||||
AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
|
AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mutableListOf(escapeLike(namespace))
|
val params = mutableListOf(escapeLike(namespace))
|
||||||
if(componentTagQuery != null) {
|
if (componentTagQuery != null) {
|
||||||
query += "\n AND ${componentTagQuery.first}"
|
query += "\n AND ${componentTagQuery.first}"
|
||||||
params += componentTagQuery.second
|
params += componentTagQuery.second
|
||||||
}
|
}
|
||||||
|
|
||||||
"$query)" to params
|
"$query)" to params
|
||||||
} else if(component != null) {
|
} else if (component != null) {
|
||||||
// Match title + tags
|
// Match title + tags
|
||||||
val tagQuery = """
|
val tagQuery = """
|
||||||
SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
||||||
@ -59,27 +61,27 @@ class SearchEngine {
|
|||||||
val include = mutableListOf<Pair<String, List<String>>>()
|
val include = mutableListOf<Pair<String, List<String>>>()
|
||||||
val exclude = mutableListOf<Pair<String, List<String>>>()
|
val exclude = mutableListOf<Pair<String, List<String>>>()
|
||||||
|
|
||||||
for(component in q) {
|
for (component in q) {
|
||||||
val query = if(component is Text) {
|
val query = if (component is Text) {
|
||||||
textToSubQueries(null, component)
|
textToSubQueries(null, component)
|
||||||
} else if(component is Namespace) {
|
} else if (component is Namespace) {
|
||||||
if(component.namespace == "uploader") {
|
if (component.namespace == "uploader") {
|
||||||
wheres += "meta.${SearchMetadataTable.COL_UPLOADER} LIKE ?"
|
wheres += "meta.${SearchMetadataTable.COL_UPLOADER} LIKE ?"
|
||||||
whereParams += component.tag!!.rawTextEscapedForLike()
|
whereParams += component.tag!!.rawTextEscapedForLike()
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
if(component.tag!!.components.size > 0) {
|
if (component.tag!!.components.size > 0) {
|
||||||
//Match namespace + tags
|
// Match namespace + tags
|
||||||
textToSubQueries(component.namespace, component.tag)
|
textToSubQueries(component.namespace, component.tag)
|
||||||
} else {
|
} else {
|
||||||
//Perform namespace search
|
// Perform namespace search
|
||||||
textToSubQueries(component.namespace, null)
|
textToSubQueries(component.namespace, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else error("Unknown query component!")
|
} else error("Unknown query component!")
|
||||||
|
|
||||||
if(query != null) {
|
if (query != null) {
|
||||||
(if(component.excluded) exclude else include) += query
|
(if (component.excluded) exclude else include) += query
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,14 +99,13 @@ class SearchEngine {
|
|||||||
completeParams += pair.second
|
completeParams += pair.second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
exclude.forEach {
|
exclude.forEach {
|
||||||
wheres += """
|
wheres += """
|
||||||
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
|
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
whereParams += it.second
|
whereParams += it.second
|
||||||
}
|
}
|
||||||
if(wheres.isNotEmpty()) {
|
if (wheres.isNotEmpty()) {
|
||||||
completeParams += whereParams
|
completeParams += whereParams
|
||||||
baseQuery += "\nWHERE\n"
|
baseQuery += "\nWHERE\n"
|
||||||
baseQuery += wheres.joinToString("\nAND\n")
|
baseQuery += wheres.joinToString("\nAND\n")
|
||||||
@ -126,7 +127,7 @@ class SearchEngine {
|
|||||||
var nextIsExact = false
|
var nextIsExact = false
|
||||||
|
|
||||||
fun flushText() {
|
fun flushText() {
|
||||||
if(queuedRawText.isNotEmpty()) {
|
if (queuedRawText.isNotEmpty()) {
|
||||||
queuedText += StringTextComponent(queuedRawText.toString())
|
queuedText += StringTextComponent(queuedRawText.toString())
|
||||||
queuedRawText.setLength(0)
|
queuedRawText.setLength(0)
|
||||||
}
|
}
|
||||||
@ -150,24 +151,24 @@ class SearchEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(char in query.toLowerCase()) {
|
for (char in query.toLowerCase()) {
|
||||||
if(char == '"') {
|
if (char == '"') {
|
||||||
inQuotes = !inQuotes
|
inQuotes = !inQuotes
|
||||||
} else if(enableWildcard && (char == '?' || char == '_')) {
|
} else if (enableWildcard && (char == '?' || char == '_')) {
|
||||||
flushText()
|
flushText()
|
||||||
queuedText.add(SingleWildcard(char.toString()))
|
queuedText.add(SingleWildcard(char.toString()))
|
||||||
} else if(enableWildcard && (char == '*' || char == '%')) {
|
} else if (enableWildcard && (char == '*' || char == '%')) {
|
||||||
flushText()
|
flushText()
|
||||||
queuedText.add(MultiWildcard(char.toString()))
|
queuedText.add(MultiWildcard(char.toString()))
|
||||||
} else if(char == '-') {
|
} else if (char == '-') {
|
||||||
nextIsExcluded = true
|
nextIsExcluded = true
|
||||||
} else if(char == '$') {
|
} else if (char == '$') {
|
||||||
nextIsExact = true
|
nextIsExact = true
|
||||||
} else if(char == ':') {
|
} else if (char == ':') {
|
||||||
flushText()
|
flushText()
|
||||||
var flushed = flushToText().rawTextOnly()
|
var flushed = flushToText().rawTextOnly()
|
||||||
//Map tag aliases
|
// Map tag aliases
|
||||||
flushed = when(flushed) {
|
flushed = when (flushed) {
|
||||||
"a" -> "artist"
|
"a" -> "artist"
|
||||||
"c", "char" -> "character"
|
"c", "char" -> "character"
|
||||||
"f" -> "female"
|
"f" -> "female"
|
||||||
@ -179,7 +180,7 @@ class SearchEngine {
|
|||||||
else -> flushed
|
else -> flushed
|
||||||
}
|
}
|
||||||
namespace = Namespace(flushed, null)
|
namespace = Namespace(flushed, null)
|
||||||
} else if(char == ' ' && !inQuotes) {
|
} else if (char == ' ' && !inQuotes) {
|
||||||
flushAll()
|
flushAll()
|
||||||
} else {
|
} else {
|
||||||
queuedRawText.append(char)
|
queuedRawText.append(char)
|
||||||
@ -197,7 +198,6 @@ class SearchEngine {
|
|||||||
return string.replace("\\", "\\\\")
|
return string.replace("\\", "\\\\")
|
||||||
.replace("_", "\\_")
|
.replace("_", "\\_")
|
||||||
.replace("%", "\\%")
|
.replace("%", "\\%")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package exh.search
|
|||||||
import exh.plusAssign
|
import exh.plusAssign
|
||||||
import exh.search.SearchEngine.Companion.escapeLike
|
import exh.search.SearchEngine.Companion.escapeLike
|
||||||
|
|
||||||
class Text: QueryComponent() {
|
class Text : QueryComponent() {
|
||||||
val components = mutableListOf<TextComponent>()
|
val components = mutableListOf<TextComponent>()
|
||||||
|
|
||||||
private var query: String? = null
|
private var query: String? = null
|
||||||
@ -12,26 +12,26 @@ class Text: QueryComponent() {
|
|||||||
private var rawText: String? = null
|
private var rawText: String? = null
|
||||||
|
|
||||||
fun asQuery(): String {
|
fun asQuery(): String {
|
||||||
if(query == null) {
|
if (query == null) {
|
||||||
query = rBaseBuilder().toString()
|
query = rBaseBuilder().toString()
|
||||||
}
|
}
|
||||||
return query!!
|
return query!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asLenientTitleQuery(): String {
|
fun asLenientTitleQuery(): String {
|
||||||
if(lenientTitleQuery == null) {
|
if (lenientTitleQuery == null) {
|
||||||
lenientTitleQuery = StringBuilder("%").append(rBaseBuilder()).append("%").toString()
|
lenientTitleQuery = StringBuilder("%").append(rBaseBuilder()).append("%").toString()
|
||||||
}
|
}
|
||||||
return lenientTitleQuery!!
|
return lenientTitleQuery!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asLenientTagQueries(): List<String> {
|
fun asLenientTagQueries(): List<String> {
|
||||||
if(lenientTagQueries == null) {
|
if (lenientTagQueries == null) {
|
||||||
lenientTagQueries = listOf(
|
lenientTagQueries = listOf(
|
||||||
//Match beginning of tag
|
// Match beginning of tag
|
||||||
rBaseBuilder().append("%").toString(),
|
rBaseBuilder().append("%").toString(),
|
||||||
//Tag word matcher (that matches multiple words)
|
// Tag word matcher (that matches multiple words)
|
||||||
//Can't make it match a single word in Realm :(
|
// Can't make it match a single word in Realm :(
|
||||||
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
|
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
|
||||||
StringBuilder(" ").append(rBaseBuilder()).toString(),
|
StringBuilder(" ").append(rBaseBuilder()).toString(),
|
||||||
rBaseBuilder().append(" ").toString()
|
rBaseBuilder().append(" ").toString()
|
||||||
@ -42,8 +42,8 @@ class Text: QueryComponent() {
|
|||||||
|
|
||||||
fun rBaseBuilder(): StringBuilder {
|
fun rBaseBuilder(): StringBuilder {
|
||||||
val builder = StringBuilder()
|
val builder = StringBuilder()
|
||||||
for(component in components) {
|
for (component in components) {
|
||||||
when(component) {
|
when (component) {
|
||||||
is StringTextComponent -> builder += escapeLike(component.value)
|
is StringTextComponent -> builder += escapeLike(component.value)
|
||||||
is SingleWildcard -> builder += "_"
|
is SingleWildcard -> builder += "_"
|
||||||
is MultiWildcard -> builder += "%"
|
is MultiWildcard -> builder += "%"
|
||||||
@ -52,7 +52,7 @@ class Text: QueryComponent() {
|
|||||||
return builder
|
return builder
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rawTextOnly() = if(rawText != null)
|
fun rawTextOnly() = if (rawText != null)
|
||||||
rawText!!
|
rawText!!
|
||||||
else {
|
else {
|
||||||
rawText = components
|
rawText = components
|
||||||
|
@ -8,13 +8,19 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import exh.ui.smartsearch.SmartSearchPresenter
|
import exh.ui.smartsearch.SmartSearchPresenter
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import info.debatty.java.stringsimilarity.NormalizedLevenshtein
|
import info.debatty.java.stringsimilarity.NormalizedLevenshtein
|
||||||
import kotlinx.coroutines.*
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
class SmartSearchEngine(parentContext: CoroutineContext,
|
class SmartSearchEngine(
|
||||||
val extraSearchParams: String? = null): CoroutineScope {
|
parentContext: CoroutineContext,
|
||||||
|
val extraSearchParams: String? = null
|
||||||
|
) : CoroutineScope {
|
||||||
override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default
|
override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default
|
||||||
|
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
@ -29,7 +35,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
|||||||
val eligibleManga = supervisorScope {
|
val eligibleManga = supervisorScope {
|
||||||
queries.map { query ->
|
queries.map { query ->
|
||||||
async(Dispatchers.Default) {
|
async(Dispatchers.Default) {
|
||||||
val builtQuery = if(extraSearchParams != null) {
|
val builtQuery = if (extraSearchParams != null) {
|
||||||
"$query ${extraSearchParams.trim()}"
|
"$query ${extraSearchParams.trim()}"
|
||||||
} else query
|
} else query
|
||||||
|
|
||||||
@ -51,7 +57,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
|||||||
|
|
||||||
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
|
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
|
||||||
val eligibleManga = supervisorScope {
|
val eligibleManga = supervisorScope {
|
||||||
val searchQuery = if(extraSearchParams != null) {
|
val searchQuery = if (extraSearchParams != null) {
|
||||||
"$title ${extraSearchParams.trim()}"
|
"$title ${extraSearchParams.trim()}"
|
||||||
} else title
|
} else title
|
||||||
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
|
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
|
||||||
@ -71,7 +77,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
|||||||
val splitCleanedTitle = cleanedTitle.split(" ")
|
val splitCleanedTitle = cleanedTitle.split(" ")
|
||||||
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
|
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
|
||||||
|
|
||||||
if(splitCleanedTitle.isEmpty()) {
|
if (splitCleanedTitle.isEmpty()) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +105,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
|||||||
|
|
||||||
// Remove text in brackets
|
// Remove text in brackets
|
||||||
var cleanedTitle = removeTextInBrackets(preTitle, true)
|
var cleanedTitle = removeTextInBrackets(preTitle, true)
|
||||||
if(cleanedTitle.length <= 5) { // Title is suspiciously short, try parsing it backwards
|
if (cleanedTitle.length <= 5) { // Title is suspiciously short, try parsing it backwards
|
||||||
cleanedTitle = removeTextInBrackets(preTitle, false)
|
cleanedTitle = removeTextInBrackets(preTitle, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +133,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
|||||||
}.toMap()
|
}.toMap()
|
||||||
|
|
||||||
// Reverse pairs if reading backwards
|
// Reverse pairs if reading backwards
|
||||||
if(!readForward) {
|
if (!readForward) {
|
||||||
val tmp = openingBracketPairs
|
val tmp = openingBracketPairs
|
||||||
openingBracketPairs = closingBracketPairs
|
openingBracketPairs = closingBracketPairs
|
||||||
closingBracketPairs = tmp
|
closingBracketPairs = tmp
|
||||||
@ -136,16 +142,16 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
|||||||
val depthPairs = bracketPairs.map { 0 }.toMutableList()
|
val depthPairs = bracketPairs.map { 0 }.toMutableList()
|
||||||
|
|
||||||
val result = StringBuilder()
|
val result = StringBuilder()
|
||||||
for(c in if(readForward) text else text.reversed()) {
|
for (c in if (readForward) text else text.reversed()) {
|
||||||
val openingBracketDepthIndex = openingBracketPairs[c]
|
val openingBracketDepthIndex = openingBracketPairs[c]
|
||||||
if(openingBracketDepthIndex != null) {
|
if (openingBracketDepthIndex != null) {
|
||||||
depthPairs[openingBracketDepthIndex]++
|
depthPairs[openingBracketDepthIndex]++
|
||||||
} else {
|
} else {
|
||||||
val closingBracketDepthIndex = closingBracketPairs[c]
|
val closingBracketDepthIndex = closingBracketPairs[c]
|
||||||
if(closingBracketDepthIndex != null) {
|
if (closingBracketDepthIndex != null) {
|
||||||
depthPairs[closingBracketDepthIndex]--
|
depthPairs[closingBracketDepthIndex]--
|
||||||
} else {
|
} else {
|
||||||
if(depthPairs.all { it <= 0 }) {
|
if (depthPairs.all { it <= 0 }) {
|
||||||
result.append(c)
|
result.append(c)
|
||||||
} else {
|
} else {
|
||||||
// In brackets, do not append to result
|
// In brackets, do not append to result
|
||||||
@ -183,4 +189,4 @@ class SmartSearchEngine(parentContext: CoroutineContext,
|
|||||||
private val titleRegex = Regex("[^a-zA-Z0-9- ]")
|
private val titleRegex = Regex("[^a-zA-Z0-9- ]")
|
||||||
private val consecutiveSpacesRegex = Regex(" +")
|
private val consecutiveSpacesRegex = Regex(" +")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,32 @@
|
|||||||
package exh.source
|
package exh.source
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.lang.RuntimeException
|
|
||||||
|
|
||||||
abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
|
abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||||
/**
|
/**
|
||||||
* Returns the request for the popular manga given the page.
|
* Returns the request for the popular manga given the page.
|
||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
override fun popularMangaRequest(page: Int)
|
override fun popularMangaRequest(page: Int) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun popularMangaParse(response: Response)
|
override fun popularMangaParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the request for the search manga given the page.
|
* Returns the request for the search manga given the page.
|
||||||
@ -32,64 +35,64 @@ abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun searchMangaParse(response: Response)
|
override fun searchMangaParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the request for latest manga given the page.
|
* Returns the request for latest manga given the page.
|
||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
override fun latestUpdatesRequest(page: Int)
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun latestUpdatesParse(response: Response)
|
override fun latestUpdatesParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns the details of a manga.
|
* Parses the response from the site and returns the details of a manga.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun mangaDetailsParse(response: Response)
|
override fun mangaDetailsParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a list of chapters.
|
* Parses the response from the site and returns a list of chapters.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun chapterListParse(response: Response)
|
override fun chapterListParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a list of pages.
|
* Parses the response from the site and returns a list of pages.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun pageListParse(response: Response)
|
override fun pageListParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns the absolute url to the source image.
|
* Parses the response from the site and returns the absolute url to the source image.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun imageUrlParse(response: Response)
|
override fun imageUrlParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||||
@ -236,8 +239,8 @@ abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
|
|||||||
override fun getFilterList() = delegate.getFilterList()
|
override fun getFilterList() = delegate.getFilterList()
|
||||||
|
|
||||||
private fun ensureDelegateCompatible() {
|
private fun ensureDelegateCompatible() {
|
||||||
if(versionId != delegate.versionId
|
if (versionId != delegate.versionId ||
|
||||||
|| lang != delegate.lang) {
|
lang != delegate.lang) {
|
||||||
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang})!")
|
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang})!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,4 +250,4 @@ abstract class DelegatedHttpSource(val delegate: HttpSource): HttpSource() {
|
|||||||
init {
|
init {
|
||||||
delegate.bindDelegate(this)
|
delegate.bindDelegate(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,19 @@ package exh.source
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class EnhancedHttpSource(val originalSource: HttpSource,
|
class EnhancedHttpSource(
|
||||||
val enchancedSource: HttpSource): HttpSource() {
|
val originalSource: HttpSource,
|
||||||
|
val enchancedSource: HttpSource
|
||||||
|
) : HttpSource() {
|
||||||
private val prefs: PreferencesHelper by injectLazy()
|
private val prefs: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,16 +22,16 @@ class EnhancedHttpSource(val originalSource: HttpSource,
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
override fun popularMangaRequest(page: Int)
|
override fun popularMangaRequest(page: Int) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun popularMangaParse(response: Response)
|
override fun popularMangaParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the request for the search manga given the page.
|
* Returns the request for the search manga given the page.
|
||||||
@ -34,64 +40,64 @@ class EnhancedHttpSource(val originalSource: HttpSource,
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun searchMangaParse(response: Response)
|
override fun searchMangaParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the request for latest manga given the page.
|
* Returns the request for latest manga given the page.
|
||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
override fun latestUpdatesRequest(page: Int)
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun latestUpdatesParse(response: Response)
|
override fun latestUpdatesParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns the details of a manga.
|
* Parses the response from the site and returns the details of a manga.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun mangaDetailsParse(response: Response)
|
override fun mangaDetailsParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a list of chapters.
|
* Parses the response from the site and returns a list of chapters.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun chapterListParse(response: Response)
|
override fun chapterListParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a list of pages.
|
* Parses the response from the site and returns a list of pages.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun pageListParse(response: Response)
|
override fun pageListParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns the absolute url to the source image.
|
* Parses the response from the site and returns the absolute url to the source image.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun imageUrlParse(response: Response)
|
override fun imageUrlParse(response: Response) =
|
||||||
= throw UnsupportedOperationException("Should never be called!")
|
throw UnsupportedOperationException("Should never be called!")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||||
@ -146,8 +152,8 @@ class EnhancedHttpSource(val originalSource: HttpSource,
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList)
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
= source().fetchSearchManga(page, query, filters)
|
source().fetchSearchManga(page, query, filters)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of latest manga updates.
|
* Returns an observable containing a page with a list of latest manga updates.
|
||||||
@ -202,8 +208,8 @@ class EnhancedHttpSource(val originalSource: HttpSource,
|
|||||||
* @param chapter the chapter to be added.
|
* @param chapter the chapter to be added.
|
||||||
* @param manga the manga of the chapter.
|
* @param manga the manga of the chapter.
|
||||||
*/
|
*/
|
||||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga)
|
override fun prepareNewChapter(chapter: SChapter, manga: SManga) =
|
||||||
= source().prepareNewChapter(chapter, manga)
|
source().prepareNewChapter(chapter, manga)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of filters for the source.
|
* Returns the list of filters for the source.
|
||||||
@ -211,10 +217,10 @@ class EnhancedHttpSource(val originalSource: HttpSource,
|
|||||||
override fun getFilterList() = source().getFilterList()
|
override fun getFilterList() = source().getFilterList()
|
||||||
|
|
||||||
private fun source(): HttpSource {
|
private fun source(): HttpSource {
|
||||||
return if(prefs.eh_delegateSources().getOrDefault()) {
|
return if (prefs.eh_delegateSources().getOrDefault()) {
|
||||||
enchancedSource
|
enchancedSource
|
||||||
} else {
|
} else {
|
||||||
originalSource
|
originalSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@ import com.afollestad.materialdialogs.MaterialDialog
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import timber.log.Timber
|
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
class ConfiguringDialogController : DialogController() {
|
class ConfiguringDialogController : DialogController() {
|
||||||
private var materialDialog: MaterialDialog? = null
|
private var materialDialog: MaterialDialog? = null
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
if(savedViewState == null)
|
if (savedViewState == null)
|
||||||
thread {
|
thread {
|
||||||
try {
|
try {
|
||||||
EHConfigurator().configureAll()
|
EHConfigurator().configureAll()
|
||||||
@ -62,4 +62,3 @@ class ConfiguringDialogController : DialogController() {
|
|||||||
router.popController(this)
|
router.popController(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,11 +24,13 @@ class EHConfigurator {
|
|||||||
private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder()
|
private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder()
|
||||||
.addHeader("Cookie", cookiesHeader(sp))
|
.addHeader("Cookie", cookiesHeader(sp))
|
||||||
|
|
||||||
private fun EHentai.execProfileActions(action: String,
|
private fun EHentai.execProfileActions(
|
||||||
name: String,
|
action: String,
|
||||||
set: String,
|
name: String,
|
||||||
sp: Int)
|
set: String,
|
||||||
= configuratorClient.newCall(requestWithCreds(sp)
|
sp: Int
|
||||||
|
) =
|
||||||
|
configuratorClient.newCall(requestWithCreds(sp)
|
||||||
.url(uconfigUrl)
|
.url(uconfigUrl)
|
||||||
.post(FormBody.Builder()
|
.post(FormBody.Builder()
|
||||||
.add("profile_action", action)
|
.add("profile_action", action)
|
||||||
@ -44,7 +46,7 @@ class EHConfigurator {
|
|||||||
val ehSource = sources.get(EH_SOURCE_ID) as EHentai
|
val ehSource = sources.get(EH_SOURCE_ID) as EHentai
|
||||||
val exhSource = sources.get(EXH_SOURCE_ID) as EHentai
|
val exhSource = sources.get(EXH_SOURCE_ID) as EHentai
|
||||||
|
|
||||||
//Get hath perks
|
// Get hath perks
|
||||||
val perksPage = configuratorClient.newCall(ehSource.requestWithCreds()
|
val perksPage = configuratorClient.newCall(ehSource.requestWithCreds()
|
||||||
.url(HATH_PERKS_URL)
|
.url(HATH_PERKS_URL)
|
||||||
.build())
|
.build())
|
||||||
@ -56,13 +58,13 @@ class EHConfigurator {
|
|||||||
val name = it.child(0).text().toLowerCase()
|
val name = it.child(0).text().toLowerCase()
|
||||||
val purchased = it.child(2).getElementsByTag("form").isEmpty()
|
val purchased = it.child(2).getElementsByTag("form").isEmpty()
|
||||||
|
|
||||||
when(name) {
|
when (name) {
|
||||||
//Thumbnail rows
|
// Thumbnail rows
|
||||||
"more thumbs" -> hathPerks.moreThumbs = purchased
|
"more thumbs" -> hathPerks.moreThumbs = purchased
|
||||||
"thumbs up" -> hathPerks.thumbsUp = purchased
|
"thumbs up" -> hathPerks.thumbsUp = purchased
|
||||||
"all thumbs" -> hathPerks.allThumbs = purchased
|
"all thumbs" -> hathPerks.allThumbs = purchased
|
||||||
|
|
||||||
//Pagination sizing
|
// Pagination sizing
|
||||||
"paging enlargement i" -> hathPerks.pagingEnlargementI = purchased
|
"paging enlargement i" -> hathPerks.pagingEnlargementI = purchased
|
||||||
"paging enlargement ii" -> hathPerks.pagingEnlargementII = purchased
|
"paging enlargement ii" -> hathPerks.pagingEnlargementII = purchased
|
||||||
"paging enlargement iii" -> hathPerks.pagingEnlargementIII = purchased
|
"paging enlargement iii" -> hathPerks.pagingEnlargementIII = purchased
|
||||||
@ -76,45 +78,45 @@ class EHConfigurator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun configure(source: EHentai, hathPerks: EHHathPerksResponse) {
|
fun configure(source: EHentai, hathPerks: EHHathPerksResponse) {
|
||||||
//Delete old app profiles
|
// Delete old app profiles
|
||||||
val scanReq = source.requestWithCreds().url(source.uconfigUrl).build()
|
val scanReq = source.requestWithCreds().url(source.uconfigUrl).build()
|
||||||
val resp = configuratorClient.newCall(scanReq).execute().asJsoup()
|
val resp = configuratorClient.newCall(scanReq).execute().asJsoup()
|
||||||
var lastDoc = resp
|
var lastDoc = resp
|
||||||
resp.select(PROFILE_SELECTOR).forEach {
|
resp.select(PROFILE_SELECTOR).forEach {
|
||||||
if(it.text() == PROFILE_NAME) {
|
if (it.text() == PROFILE_NAME) {
|
||||||
val id = it.attr("value")
|
val id = it.attr("value")
|
||||||
//Delete old profile
|
// Delete old profile
|
||||||
lastDoc = source.execProfileActions("delete", "", id, id.toInt()).asJsoup()
|
lastDoc = source.execProfileActions("delete", "", id, id.toInt()).asJsoup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Find available profile slot
|
// Find available profile slot
|
||||||
val availableProfiles = (1 .. 3).toMutableList()
|
val availableProfiles = (1..3).toMutableList()
|
||||||
lastDoc.select(PROFILE_SELECTOR).forEach {
|
lastDoc.select(PROFILE_SELECTOR).forEach {
|
||||||
availableProfiles.remove(it.attr("value").toInt())
|
availableProfiles.remove(it.attr("value").toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
//No profile slots left :(
|
// No profile slots left :(
|
||||||
if(availableProfiles.isEmpty())
|
if (availableProfiles.isEmpty())
|
||||||
throw IllegalStateException("You are out of profile slots on ${source.name}, please delete a profile!")
|
throw IllegalStateException("You are out of profile slots on ${source.name}, please delete a profile!")
|
||||||
|
|
||||||
//Create profile in available slot
|
// Create profile in available slot
|
||||||
val slot = availableProfiles.first()
|
val slot = availableProfiles.first()
|
||||||
val response = source.execProfileActions("create",
|
val response = source.execProfileActions("create",
|
||||||
PROFILE_NAME,
|
PROFILE_NAME,
|
||||||
slot.toString(),
|
slot.toString(),
|
||||||
1)
|
1)
|
||||||
|
|
||||||
//Build new profile
|
// Build new profile
|
||||||
val form = EhUConfigBuilder().build(hathPerks)
|
val form = EhUConfigBuilder().build(hathPerks)
|
||||||
|
|
||||||
//Send new profile to server
|
// Send new profile to server
|
||||||
configuratorClient.newCall(source.requestWithCreds(sp = slot)
|
configuratorClient.newCall(source.requestWithCreds(sp = slot)
|
||||||
.url(source.uconfigUrl)
|
.url(source.uconfigUrl)
|
||||||
.post(form)
|
.post(form)
|
||||||
.build()).execute()
|
.build()).execute()
|
||||||
|
|
||||||
//Persist slot + sk
|
// Persist slot + sk
|
||||||
source.spPref().set(slot)
|
source.spPref().set(slot)
|
||||||
|
|
||||||
val keyCookie = response.headers.toMultimap()["Set-Cookie"]?.find {
|
val keyCookie = response.headers.toMultimap()["Set-Cookie"]?.find {
|
||||||
@ -127,19 +129,19 @@ class EHConfigurator {
|
|||||||
it.startsWith("hath_perks=")
|
it.startsWith("hath_perks=")
|
||||||
}?.removePrefix("hath_perks=")?.substringBefore(';')
|
}?.removePrefix("hath_perks=")?.substringBefore(';')
|
||||||
|
|
||||||
if(keyCookie != null)
|
if (keyCookie != null)
|
||||||
prefs.eh_settingsKey().set(keyCookie)
|
prefs.eh_settingsKey().set(keyCookie)
|
||||||
if(sessionCookie != null)
|
if (sessionCookie != null)
|
||||||
prefs.eh_sessionCookie().set(sessionCookie)
|
prefs.eh_sessionCookie().set(sessionCookie)
|
||||||
if(hathPerksCookie != null)
|
if (hathPerksCookie != null)
|
||||||
prefs.eh_hathPerksCookies().set(hathPerksCookie)
|
prefs.eh_hathPerksCookies().set(hathPerksCookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PROFILE_NAME = "TachiyomiEH App"
|
private const val PROFILE_NAME = "TachiyomiEH App"
|
||||||
private const val UCONFIG_URL = "/uconfig.php"
|
private const val UCONFIG_URL = "/uconfig.php"
|
||||||
//Always use E-H here as EXH does not have a perks page
|
// Always use E-H here as EXH does not have a perks page
|
||||||
private const val HATH_PERKS_URL = "https://e-hentai.org/hathperks.php"
|
private const val HATH_PERKS_URL = "https://e-hentai.org/hathperks.php"
|
||||||
private const val PROFILE_SELECTOR = "[name=profile_set] > option"
|
private const val PROFILE_SELECTOR = "[name=profile_set] > option"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ class EHHathPerksResponse {
|
|||||||
var pagingEnlargementII = false
|
var pagingEnlargementII = false
|
||||||
var pagingEnlargementIII = false
|
var pagingEnlargementIII = false
|
||||||
|
|
||||||
override fun toString()
|
override fun toString() =
|
||||||
= "EHHathPerksResponse(moreThumbs=$moreThumbs, thumbsUp=$thumbsUp, allThumbs=$allThumbs, pagingEnlargementI=$pagingEnlargementI, pagingEnlargementII=$pagingEnlargementII, pagingEnlargementIII=$pagingEnlargementIII)"
|
"EHHathPerksResponse(moreThumbs=$moreThumbs, thumbsUp=$thumbsUp, allThumbs=$allThumbs, pagingEnlargementI=$pagingEnlargementI, pagingEnlargementII=$pagingEnlargementII, pagingEnlargementIII=$pagingEnlargementIII)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class EhUConfigBuilder {
|
|||||||
fun build(hathPerks: EHHathPerksResponse): FormBody {
|
fun build(hathPerks: EHHathPerksResponse): FormBody {
|
||||||
val configItems = mutableListOf<ConfigItem>()
|
val configItems = mutableListOf<ConfigItem>()
|
||||||
|
|
||||||
configItems += when(prefs.imageQuality()
|
configItems += when (prefs.imageQuality()
|
||||||
.getOrDefault()
|
.getOrDefault()
|
||||||
.toLowerCase()) {
|
.toLowerCase()) {
|
||||||
"ovrs_2400" -> Entry.ImageSize.`2400`
|
"ovrs_2400" -> Entry.ImageSize.`2400`
|
||||||
@ -23,17 +23,17 @@ class EhUConfigBuilder {
|
|||||||
else -> Entry.ImageSize.AUTO
|
else -> Entry.ImageSize.AUTO
|
||||||
}
|
}
|
||||||
|
|
||||||
configItems += if(prefs.useHentaiAtHome().getOrDefault())
|
configItems += if (prefs.useHentaiAtHome().getOrDefault())
|
||||||
Entry.UseHentaiAtHome.YES
|
Entry.UseHentaiAtHome.YES
|
||||||
else
|
else
|
||||||
Entry.UseHentaiAtHome.NO
|
Entry.UseHentaiAtHome.NO
|
||||||
|
|
||||||
configItems += if(prefs.useJapaneseTitle().getOrDefault())
|
configItems += if (prefs.useJapaneseTitle().getOrDefault())
|
||||||
Entry.TitleDisplayLanguage.JAPANESE
|
Entry.TitleDisplayLanguage.JAPANESE
|
||||||
else
|
else
|
||||||
Entry.TitleDisplayLanguage.DEFAULT
|
Entry.TitleDisplayLanguage.DEFAULT
|
||||||
|
|
||||||
configItems += if(prefs.eh_useOriginalImages().getOrDefault())
|
configItems += if (prefs.eh_useOriginalImages().getOrDefault())
|
||||||
Entry.UseOriginalImages.YES
|
Entry.UseOriginalImages.YES
|
||||||
else
|
else
|
||||||
Entry.UseOriginalImages.NO
|
Entry.UseOriginalImages.NO
|
||||||
@ -56,7 +56,7 @@ class EhUConfigBuilder {
|
|||||||
configItems += Entry.UseMPV()
|
configItems += Entry.UseMPV()
|
||||||
configItems += Entry.ShowPopularRightNowPane()
|
configItems += Entry.ShowPopularRightNowPane()
|
||||||
|
|
||||||
//Actually build form body
|
// Actually build form body
|
||||||
val formBody = FormBody.Builder()
|
val formBody = FormBody.Builder()
|
||||||
configItems.forEach {
|
configItems.forEach {
|
||||||
formBody.add(it.key, it.value)
|
formBody.add(it.key, it.value)
|
||||||
@ -67,14 +67,14 @@ class EhUConfigBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Entry {
|
object Entry {
|
||||||
enum class UseHentaiAtHome(override val value: String): ConfigItem {
|
enum class UseHentaiAtHome(override val value: String) : ConfigItem {
|
||||||
YES("0"),
|
YES("0"),
|
||||||
NO("1");
|
NO("1");
|
||||||
|
|
||||||
override val key = "uh"
|
override val key = "uh"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ImageSize(override val value: String): ConfigItem {
|
enum class ImageSize(override val value: String) : ConfigItem {
|
||||||
AUTO("0"),
|
AUTO("0"),
|
||||||
`2400`("5"),
|
`2400`("5"),
|
||||||
`1600`("4"),
|
`1600`("4"),
|
||||||
@ -85,20 +85,20 @@ object Entry {
|
|||||||
override val key = "xr"
|
override val key = "xr"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class TitleDisplayLanguage(override val value: String): ConfigItem {
|
enum class TitleDisplayLanguage(override val value: String) : ConfigItem {
|
||||||
DEFAULT("0"),
|
DEFAULT("0"),
|
||||||
JAPANESE("1");
|
JAPANESE("1");
|
||||||
|
|
||||||
override val key = "tl"
|
override val key = "tl"
|
||||||
}
|
}
|
||||||
|
|
||||||
//Locked to extended mode as that's what the parser and toplists use
|
// Locked to extended mode as that's what the parser and toplists use
|
||||||
class DisplayMode: ConfigItem {
|
class DisplayMode : ConfigItem {
|
||||||
override val key = "dm"
|
override val key = "dm"
|
||||||
override val value = "2"
|
override val value = "2"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SearchResultsCount(override val value: String): ConfigItem {
|
enum class SearchResultsCount(override val value: String) : ConfigItem {
|
||||||
`25`("0"),
|
`25`("0"),
|
||||||
`50`("1"),
|
`50`("1"),
|
||||||
`100`("2"),
|
`100`("2"),
|
||||||
@ -107,7 +107,7 @@ object Entry {
|
|||||||
override val key = "rc"
|
override val key = "rc"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ThumbnailRows(override val value: String): ConfigItem {
|
enum class ThumbnailRows(override val value: String) : ConfigItem {
|
||||||
`4`("0"),
|
`4`("0"),
|
||||||
`10`("1"),
|
`10`("1"),
|
||||||
`20`("2"),
|
`20`("2"),
|
||||||
@ -116,21 +116,21 @@ object Entry {
|
|||||||
override val key = "tr"
|
override val key = "tr"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class UseOriginalImages(override val value: String): ConfigItem {
|
enum class UseOriginalImages(override val value: String) : ConfigItem {
|
||||||
NO("0"),
|
NO("0"),
|
||||||
YES("1");
|
YES("1");
|
||||||
|
|
||||||
override val key = "oi"
|
override val key = "oi"
|
||||||
}
|
}
|
||||||
|
|
||||||
//Locked to no MPV as that's what the parser uses
|
// Locked to no MPV as that's what the parser uses
|
||||||
class UseMPV: ConfigItem {
|
class UseMPV : ConfigItem {
|
||||||
override val key = "qb"
|
override val key = "qb"
|
||||||
override val value = "0"
|
override val value = "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
//Locked to no popular pane as we can't parse it
|
// Locked to no popular pane as we can't parse it
|
||||||
class ShowPopularRightNowPane: ConfigItem {
|
class ShowPopularRightNowPane : ConfigItem {
|
||||||
override val key = "pp"
|
override val key = "pp"
|
||||||
override val value = "1"
|
override val value = "1"
|
||||||
}
|
}
|
||||||
@ -139,4 +139,4 @@ object Entry {
|
|||||||
interface ConfigItem {
|
interface ConfigItem {
|
||||||
val key: String
|
val key: String
|
||||||
val value: String
|
val value: String
|
||||||
}
|
}
|
||||||
|
@ -32,10 +32,10 @@ class WarnConfigureDialogController : DialogController() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun uploadSettings(router: Router) {
|
fun uploadSettings(router: Router) {
|
||||||
if(Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().getOrDefault())
|
if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().getOrDefault())
|
||||||
WarnConfigureDialogController().showDialog(router)
|
WarnConfigureDialogController().showDialog(router)
|
||||||
else
|
else
|
||||||
ConfiguringDialogController().showDialog(router)
|
ConfiguringDialogController().showDialog(router)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
package exh.ui
|
package exh.ui
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
typealias LoadingHandle = String
|
typealias LoadingHandle = String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to manage loader UIs
|
* Class used to manage loader UIs
|
||||||
*/
|
*/
|
||||||
class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): CoroutineScope {
|
class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope {
|
||||||
override val coroutineContext = Dispatchers.Main + parentContext
|
override val coroutineContext = Dispatchers.Main + parentContext
|
||||||
|
|
||||||
private val openLoadingHandles = mutableListOf<LoadingHandle>()
|
private val openLoadingHandles = mutableListOf<LoadingHandle>()
|
||||||
@ -25,7 +25,7 @@ class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): Co
|
|||||||
handle to (openLoadingHandles.size == 1)
|
handle to (openLoadingHandles.size == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(shouldUpdateLoadingStatus) {
|
if (shouldUpdateLoadingStatus) {
|
||||||
launch {
|
launch {
|
||||||
updateLoadingStatus(true)
|
updateLoadingStatus(true)
|
||||||
}
|
}
|
||||||
@ -36,13 +36,13 @@ class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): Co
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun closeProgressBar(handle: LoadingHandle?) {
|
fun closeProgressBar(handle: LoadingHandle?) {
|
||||||
if(handle == null) return
|
if (handle == null) return
|
||||||
|
|
||||||
val shouldUpdateLoadingStatus = synchronized(this) {
|
val shouldUpdateLoadingStatus = synchronized(this) {
|
||||||
openLoadingHandles.remove(handle) && openLoadingHandles.isEmpty()
|
openLoadingHandles.remove(handle) && openLoadingHandles.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(shouldUpdateLoadingStatus) {
|
if (shouldUpdateLoadingStatus) {
|
||||||
launch {
|
launch {
|
||||||
updateLoadingStatus(false)
|
updateLoadingStatus(false)
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,11 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
abstract class BaseExhController(bundle: Bundle? = null) : BaseController(bundle), CoroutineScope {
|
abstract class BaseExhController(bundle: Bundle? = null) : BaseController(bundle), CoroutineScope {
|
||||||
abstract val layoutId: Int
|
abstract val layoutId: Int
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package exh.ui.batchadd
|
package exh.ui.batchadd
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -44,14 +43,14 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeUntilDestroy {
|
.subscribeUntilDestroy {
|
||||||
progressSubscriptions.clear()
|
progressSubscriptions.clear()
|
||||||
if(it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
|
if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
|
||||||
showProgress(this)
|
showProgress(this)
|
||||||
progressSubscriptions += presenter.progressRelay
|
progressSubscriptions += presenter.progressRelay
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.combineLatest(presenter.progressTotalRelay, { progress, total ->
|
.combineLatest(presenter.progressTotalRelay, { progress, total ->
|
||||||
//Show hide dismiss button
|
// Show hide dismiss button
|
||||||
progress_dismiss_btn.visibility =
|
progress_dismiss_btn.visibility =
|
||||||
if(progress == total)
|
if (progress == total)
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
else View.GONE
|
else View.GONE
|
||||||
|
|
||||||
@ -79,7 +78,7 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
|
|||||||
}?.let {
|
}?.let {
|
||||||
progressSubscriptions += it
|
progressSubscriptions += it
|
||||||
}
|
}
|
||||||
} else if(it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
|
} else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
|
||||||
hideProgress(this)
|
hideProgress(this)
|
||||||
presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE)
|
presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE)
|
||||||
}
|
}
|
||||||
@ -124,8 +123,8 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
|
|||||||
private fun formatProgress(progress: Int, total: Int) = "$progress/$total"
|
private fun formatProgress(progress: Int, total: Int) = "$progress/$total"
|
||||||
|
|
||||||
private fun addGalleries(galleries: String) {
|
private fun addGalleries(galleries: String) {
|
||||||
//Check text box has content
|
// Check text box has content
|
||||||
if(galleries.isBlank()) {
|
if (galleries.isBlank()) {
|
||||||
noGalleriesSpecified()
|
noGalleriesSpecified()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import exh.GalleryAdder
|
|||||||
import exh.metadata.nullIfBlank
|
import exh.metadata.nullIfBlank
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class BatchAddPresenter: BasePresenter<BatchAddController>() {
|
class BatchAddPresenter : BasePresenter<BatchAddController>() {
|
||||||
|
|
||||||
private val galleryAdder by lazy { GalleryAdder() }
|
private val galleryAdder by lazy { GalleryAdder() }
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
|
|||||||
|
|
||||||
splitGalleries.forEachIndexed { i, s ->
|
splitGalleries.forEachIndexed { i, s ->
|
||||||
val result = galleryAdder.addGallery(s, true)
|
val result = galleryAdder.addGallery(s, true)
|
||||||
if(result is GalleryAddEvent.Success) {
|
if (result is GalleryAddEvent.Success) {
|
||||||
succeeded.add(s)
|
succeeded.add(s)
|
||||||
} else {
|
} else {
|
||||||
failed.add(s)
|
failed.add(s)
|
||||||
@ -46,7 +46,7 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
|
|||||||
}) + " " + result.logMessage)
|
}) + " " + result.logMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Show report
|
// Show report
|
||||||
val summary = "\nSummary:\nAdded: ${succeeded.size} gallerie(s)\nFailed: ${failed.size} gallerie(s)"
|
val summary = "\nSummary:\nAdded: ${succeeded.size} gallerie(s)\nFailed: ${failed.size} gallerie(s)"
|
||||||
eventRelay?.call(summary)
|
eventRelay?.call(summary)
|
||||||
}
|
}
|
||||||
|
@ -7,21 +7,23 @@ import android.webkit.WebView
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import eu.kanade.tachiyomi.util.system.asJsoup
|
import eu.kanade.tachiyomi.util.system.asJsoup
|
||||||
import exh.ui.captcha.BrowserActionActivity.Companion.CROSS_WINDOW_SCRIPT_INNER
|
import exh.ui.captcha.BrowserActionActivity.Companion.CROSS_WINDOW_SCRIPT_INNER
|
||||||
|
import java.nio.charset.Charset
|
||||||
import org.jsoup.nodes.DataNode
|
import org.jsoup.nodes.DataNode
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.nio.charset.Charset
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
class AutoSolvingWebViewClient(activity: BrowserActionActivity,
|
class AutoSolvingWebViewClient(
|
||||||
verifyComplete: (String) -> Boolean,
|
activity: BrowserActionActivity,
|
||||||
injectScript: String?,
|
verifyComplete: (String) -> Boolean,
|
||||||
headers: Map<String, String>)
|
injectScript: String?,
|
||||||
: HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) {
|
headers: Map<String, String>
|
||||||
|
) :
|
||||||
|
HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) {
|
||||||
|
|
||||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||||||
// Inject our custom script into the recaptcha iframes
|
// Inject our custom script into the recaptcha iframes
|
||||||
val lastPathSegment = request.url.pathSegments.lastOrNull()
|
val lastPathSegment = request.url.pathSegments.lastOrNull()
|
||||||
if(lastPathSegment == "anchor" || lastPathSegment == "bframe") {
|
if (lastPathSegment == "anchor" || lastPathSegment == "bframe") {
|
||||||
val oReq = request.toOkHttpRequest()
|
val oReq = request.toOkHttpRequest()
|
||||||
val response = activity.httpClient.newCall(oReq).execute()
|
val response = activity.httpClient.newCall(oReq).execute()
|
||||||
val doc = response.asJsoup()
|
val doc = response.asJsoup()
|
||||||
@ -34,4 +36,4 @@ class AutoSolvingWebViewClient(activity: BrowserActionActivity,
|
|||||||
}
|
}
|
||||||
return super.shouldInterceptRequest(view, request)
|
return super.shouldInterceptRequest(view, request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,17 +4,19 @@ import android.os.Build
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
|
|
||||||
open class BasicWebViewClient(protected val activity: BrowserActionActivity,
|
open class BasicWebViewClient(
|
||||||
protected val verifyComplete: (String) -> Boolean,
|
protected val activity: BrowserActionActivity,
|
||||||
private val injectScript: String?) : WebViewClient() {
|
protected val verifyComplete: (String) -> Boolean,
|
||||||
|
private val injectScript: String?
|
||||||
|
) : WebViewClient() {
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
super.onPageFinished(view, url)
|
super.onPageFinished(view, url)
|
||||||
|
|
||||||
if(verifyComplete(url)) {
|
if (verifyComplete(url)) {
|
||||||
activity.finish()
|
activity.finish()
|
||||||
} else {
|
} else {
|
||||||
if(injectScript != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
if (injectScript != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||||
view.evaluateJavascript("(function() {$injectScript})();", null)
|
view.evaluateJavascript("(function() {$injectScript})();", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,12 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.webkit.*
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.CookieSyncManager
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.JsResult
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebView
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
@ -22,6 +27,9 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import exh.source.DelegatedHttpSource
|
import exh.source.DelegatedHttpSource
|
||||||
import exh.util.melt
|
import exh.util.melt
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.UUID
|
||||||
import kotlinx.android.synthetic.main.eh_activity_captcha.*
|
import kotlinx.android.synthetic.main.eh_activity_captcha.*
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
@ -33,10 +41,6 @@ import rx.Single
|
|||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.Serializable
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.HashMap
|
|
||||||
|
|
||||||
class BrowserActionActivity : AppCompatActivity() {
|
class BrowserActionActivity : AppCompatActivity() {
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
@ -58,8 +62,8 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha)
|
setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha)
|
||||||
|
|
||||||
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
|
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
|
||||||
val originalSource = if(sourceId != -1L) sourceManager.get(sourceId) else null
|
val originalSource = if (sourceId != -1L) sourceManager.get(sourceId) else null
|
||||||
val source = if(originalSource != null) {
|
val source = if (originalSource != null) {
|
||||||
originalSource as? ActionCompletionVerifier
|
originalSource as? ActionCompletionVerifier
|
||||||
?: run {
|
?: run {
|
||||||
(originalSource as? HttpSource)?.let {
|
(originalSource as? HttpSource)?.let {
|
||||||
@ -72,24 +76,24 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
it.value.joinToString(",")
|
it.value.joinToString(",")
|
||||||
} ?: emptyMap()) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
|
} ?: emptyMap()) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
|
||||||
|
|
||||||
val cookies: HashMap<String, String>?
|
val cookies: HashMap<String, String>? =
|
||||||
= intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
||||||
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
|
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
|
||||||
val url: String? = intent.getStringExtra(URL_EXTRA)
|
val url: String? = intent.getStringExtra(URL_EXTRA)
|
||||||
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
|
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
|
||||||
|
|
||||||
val verifyComplete = if(source != null) {
|
val verifyComplete = if (source != null) {
|
||||||
source::verifyComplete!!
|
source::verifyComplete!!
|
||||||
} else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean
|
} else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean
|
||||||
|
|
||||||
if(verifyComplete == null || url == null) {
|
if (verifyComplete == null || url == null) {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val actionStr = actionName ?: "Solve captcha"
|
val actionStr = actionName ?: "Solve captcha"
|
||||||
|
|
||||||
toolbar.title = if(source != null) {
|
toolbar.title = if (source != null) {
|
||||||
"${source.name}: $actionStr"
|
"${source.name}: $actionStr"
|
||||||
} else actionStr
|
} else actionStr
|
||||||
|
|
||||||
@ -115,13 +119,13 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
webview.webChromeClient = object : WebChromeClient() {
|
webview.webChromeClient = object : WebChromeClient() {
|
||||||
override fun onJsAlert(view: WebView?, url: String?, message: String, result: JsResult): Boolean {
|
override fun onJsAlert(view: WebView?, url: String?, message: String, result: JsResult): Boolean {
|
||||||
if(message.startsWith("exh-")) {
|
if (message.startsWith("exh-")) {
|
||||||
loadedInners++
|
loadedInners++
|
||||||
// Wait for both inner scripts to be loaded
|
// Wait for both inner scripts to be loaded
|
||||||
if(loadedInners >= 2) {
|
if (loadedInners >= 2) {
|
||||||
// Attempt to autosolve captcha
|
// Attempt to autosolve captcha
|
||||||
if(preferencesHelper.eh_autoSolveCaptchas().getOrDefault()
|
if (preferencesHelper.eh_autoSolveCaptchas().getOrDefault() &&
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
webview.post {
|
webview.post {
|
||||||
// 10 seconds to auto-solve captcha
|
// 10 seconds to auto-solve captcha
|
||||||
strictValidationStartTime = System.currentTimeMillis() + 1000 * 10
|
strictValidationStartTime = System.currentTimeMillis() + 1000 * 10
|
||||||
@ -141,7 +145,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
webview.webViewClient = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
webview.webViewClient = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if(actionName == null && preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) {
|
if (actionName == null && preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) {
|
||||||
// Fetch auto-solve credentials early for speed
|
// Fetch auto-solve credentials early for speed
|
||||||
credentialsObservable = httpClient.newCall(Request.Builder()
|
credentialsObservable = httpClient.newCall(Request.Builder()
|
||||||
// Rob demo credentials
|
// Rob demo credentials
|
||||||
@ -196,11 +200,11 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun callback(result: String?, loopId: String, stage: Int) {
|
fun callback(result: String?, loopId: String, stage: Int) {
|
||||||
if(loopId != currentLoopId) return
|
if (loopId != currentLoopId) return
|
||||||
|
|
||||||
when(stage) {
|
when (stage) {
|
||||||
STAGE_CHECKBOX -> {
|
STAGE_CHECKBOX -> {
|
||||||
if(result!!.toBoolean()) {
|
if (result!!.toBoolean()) {
|
||||||
webview.postDelayed({
|
webview.postDelayed({
|
||||||
getAudioButtonLocation(loopId)
|
getAudioButtonLocation(loopId)
|
||||||
}, 250)
|
}, 250)
|
||||||
@ -211,7 +215,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
STAGE_GET_AUDIO_BTN_LOCATION -> {
|
STAGE_GET_AUDIO_BTN_LOCATION -> {
|
||||||
if(result != null) {
|
if (result != null) {
|
||||||
val splitResult = result.split(" ").map { it.toFloat() }
|
val splitResult = result.split(" ").map { it.toFloat() }
|
||||||
val origX = splitResult[0]
|
val origX = splitResult[0]
|
||||||
val origY = splitResult[1]
|
val origY = splitResult[1]
|
||||||
@ -231,11 +235,11 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
STAGE_DOWNLOAD_AUDIO -> {
|
STAGE_DOWNLOAD_AUDIO -> {
|
||||||
if(result != null) {
|
if (result != null) {
|
||||||
Timber.d("Got audio URL: $result")
|
Timber.d("Got audio URL: $result")
|
||||||
performRecognize(result)
|
performRecognize(result)
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.subscribe ({
|
.subscribe({
|
||||||
Timber.d("Got audio transcript: $it")
|
Timber.d("Got audio transcript: $it")
|
||||||
webview.post {
|
webview.post {
|
||||||
typeResult(loopId, it!!
|
typeResult(loopId, it!!
|
||||||
@ -253,7 +257,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
STAGE_TYPE_RESULT -> {
|
STAGE_TYPE_RESULT -> {
|
||||||
if(result!!.toBoolean()) {
|
if (result!!.toBoolean()) {
|
||||||
// Fail if captcha still not solved after 1.5s
|
// Fail if captcha still not solved after 1.5s
|
||||||
strictValidationStartTime = System.currentTimeMillis() + 1500
|
strictValidationStartTime = System.currentTimeMillis() + 1500
|
||||||
} else {
|
} else {
|
||||||
@ -293,7 +297,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
fun doStageCheckbox(loopId: String) {
|
fun doStageCheckbox(loopId: String) {
|
||||||
if(loopId != currentLoopId) return
|
if (loopId != currentLoopId) return
|
||||||
|
|
||||||
webview.evaluateJavascript("""
|
webview.evaluateJavascript("""
|
||||||
(function() {
|
(function() {
|
||||||
@ -415,27 +419,26 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
doStageCheckbox(loopId)
|
doStageCheckbox(loopId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun validateCaptchaCallback(result: Boolean, loopId: String) {
|
fun validateCaptchaCallback(result: Boolean, loopId: String) {
|
||||||
if(loopId != validateCurrentLoopId) return
|
if (loopId != validateCurrentLoopId) return
|
||||||
|
|
||||||
if(result) {
|
if (result) {
|
||||||
Timber.d("Captcha solved!")
|
Timber.d("Captcha solved!")
|
||||||
webview.post {
|
webview.post {
|
||||||
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
|
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
|
||||||
}
|
}
|
||||||
val asbtn = intent.getStringExtra(ASBTN_EXTRA)
|
val asbtn = intent.getStringExtra(ASBTN_EXTRA)
|
||||||
if(asbtn != null) {
|
if (asbtn != null) {
|
||||||
webview.post {
|
webview.post {
|
||||||
webview.evaluateJavascript("(function() {document.querySelector('$asbtn').click();})();", null)
|
webview.evaluateJavascript("(function() {document.querySelector('$asbtn').click();})();", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val savedStrictValidationStartTime = strictValidationStartTime
|
val savedStrictValidationStartTime = strictValidationStartTime
|
||||||
if(savedStrictValidationStartTime != null
|
if (savedStrictValidationStartTime != null &&
|
||||||
&& System.currentTimeMillis() > savedStrictValidationStartTime) {
|
System.currentTimeMillis() > savedStrictValidationStartTime) {
|
||||||
captchaSolveFail()
|
captchaSolveFail()
|
||||||
} else {
|
} else {
|
||||||
webview.postDelayed({
|
webview.postDelayed({
|
||||||
@ -447,7 +450,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
fun runValidateCaptcha(loopId: String) {
|
fun runValidateCaptcha(loopId: String) {
|
||||||
if(loopId != validateCurrentLoopId) return
|
if (loopId != validateCurrentLoopId) return
|
||||||
|
|
||||||
webview.evaluateJavascript("""
|
webview.evaluateJavascript("""
|
||||||
(function() {
|
(function() {
|
||||||
@ -624,12 +627,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchCaptcha(context: Context,
|
fun launchCaptcha(
|
||||||
source: ActionCompletionVerifier,
|
context: Context,
|
||||||
cookies: Map<String, String>,
|
source: ActionCompletionVerifier,
|
||||||
script: String?,
|
cookies: Map<String, String>,
|
||||||
url: String,
|
script: String?,
|
||||||
autoSolveSubmitBtnSelector: String? = null) {
|
url: String,
|
||||||
|
autoSolveSubmitBtnSelector: String? = null
|
||||||
|
) {
|
||||||
val intent = baseIntent(context).apply {
|
val intent = baseIntent(context).apply {
|
||||||
putExtra(SOURCE_ID_EXTRA, source.id)
|
putExtra(SOURCE_ID_EXTRA, source.id)
|
||||||
putExtra(COOKIES_EXTRA, HashMap(cookies))
|
putExtra(COOKIES_EXTRA, HashMap(cookies))
|
||||||
@ -641,9 +646,11 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchUniversal(context: Context,
|
fun launchUniversal(
|
||||||
source: HttpSource,
|
context: Context,
|
||||||
url: String) {
|
source: HttpSource,
|
||||||
|
url: String
|
||||||
|
) {
|
||||||
val intent = baseIntent(context).apply {
|
val intent = baseIntent(context).apply {
|
||||||
putExtra(SOURCE_ID_EXTRA, source.id)
|
putExtra(SOURCE_ID_EXTRA, source.id)
|
||||||
putExtra(URL_EXTRA, url)
|
putExtra(URL_EXTRA, url)
|
||||||
@ -652,9 +659,11 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchUniversal(context: Context,
|
fun launchUniversal(
|
||||||
sourceId: Long,
|
context: Context,
|
||||||
url: String) {
|
sourceId: Long,
|
||||||
|
url: String
|
||||||
|
) {
|
||||||
val intent = baseIntent(context).apply {
|
val intent = baseIntent(context).apply {
|
||||||
putExtra(SOURCE_ID_EXTRA, sourceId)
|
putExtra(SOURCE_ID_EXTRA, sourceId)
|
||||||
putExtra(URL_EXTRA, url)
|
putExtra(URL_EXTRA, url)
|
||||||
@ -663,11 +672,13 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchAction(context: Context,
|
fun launchAction(
|
||||||
completionVerifier: ActionCompletionVerifier,
|
context: Context,
|
||||||
script: String?,
|
completionVerifier: ActionCompletionVerifier,
|
||||||
url: String,
|
script: String?,
|
||||||
actionName: String) {
|
url: String,
|
||||||
|
actionName: String
|
||||||
|
) {
|
||||||
val intent = baseIntent(context).apply {
|
val intent = baseIntent(context).apply {
|
||||||
putExtra(SOURCE_ID_EXTRA, completionVerifier.id)
|
putExtra(SOURCE_ID_EXTRA, completionVerifier.id)
|
||||||
putExtra(SCRIPT_EXTRA, script)
|
putExtra(SCRIPT_EXTRA, script)
|
||||||
@ -678,12 +689,14 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchAction(context: Context,
|
fun launchAction(
|
||||||
completionVerifier: (String) -> Boolean,
|
context: Context,
|
||||||
script: String?,
|
completionVerifier: (String) -> Boolean,
|
||||||
url: String,
|
script: String?,
|
||||||
actionName: String,
|
url: String,
|
||||||
headers: Map<String, String>? = emptyMap()) {
|
actionName: String,
|
||||||
|
headers: Map<String, String>? = emptyMap()
|
||||||
|
) {
|
||||||
val intent = baseIntent(context).apply {
|
val intent = baseIntent(context).apply {
|
||||||
putExtra(HEADERS_EXTRA, HashMap(headers))
|
putExtra(HEADERS_EXTRA, HashMap(headers))
|
||||||
putExtra(VERIFY_LAMBDA_EXTRA, completionVerifier as Serializable)
|
putExtra(VERIFY_LAMBDA_EXTRA, completionVerifier as Serializable)
|
||||||
@ -697,7 +710,7 @@ class BrowserActionActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoopActionCompletionVerifier(private val source: HttpSource): DelegatedHttpSource(source),
|
class NoopActionCompletionVerifier(private val source: HttpSource) : DelegatedHttpSource(source),
|
||||||
ActionCompletionVerifier {
|
ActionCompletionVerifier {
|
||||||
override val versionId get() = source.versionId
|
override val versionId get() = source.versionId
|
||||||
override val lang: String get() = source.lang
|
override val lang: String get() = source.lang
|
||||||
@ -708,4 +721,3 @@ class NoopActionCompletionVerifier(private val source: HttpSource): DelegatedHtt
|
|||||||
interface ActionCompletionVerifier : Source {
|
interface ActionCompletionVerifier : Source {
|
||||||
fun verifyComplete(url: String): Boolean
|
fun verifyComplete(url: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,11 +7,13 @@ import android.webkit.WebView
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
open class HeadersInjectingWebViewClient(activity: BrowserActionActivity,
|
open class HeadersInjectingWebViewClient(
|
||||||
verifyComplete: (String) -> Boolean,
|
activity: BrowserActionActivity,
|
||||||
injectScript: String?,
|
verifyComplete: (String) -> Boolean,
|
||||||
private val headers: Map<String, String>)
|
injectScript: String?,
|
||||||
: BasicWebViewClient(activity, verifyComplete, injectScript) {
|
private val headers: Map<String, String>
|
||||||
|
) :
|
||||||
|
BasicWebViewClient(activity, verifyComplete, injectScript) {
|
||||||
|
|
||||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||||||
// Temp disabled as it's unreliable
|
// Temp disabled as it's unreliable
|
||||||
|
@ -16,4 +16,4 @@ fun WebResourceRequest.toOkHttpRequest(): Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return request.build()
|
return request.build()
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.eh_activity_intercept)
|
setContentView(R.layout.eh_activity_intercept)
|
||||||
|
|
||||||
//Show back button
|
// Show back button
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun processLink() {
|
private fun processLink() {
|
||||||
if(Intent.ACTION_VIEW == intent.action) {
|
if (Intent.ACTION_VIEW == intent.action) {
|
||||||
intercept_progress.visible()
|
intercept_progress.visible()
|
||||||
intercept_status.text = "Loading gallery..."
|
intercept_status.text = "Loading gallery..."
|
||||||
presenter.loadGallery(intent.dataString)
|
presenter.loadGallery(intent.dataString)
|
||||||
@ -52,7 +52,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
|
|||||||
statusSubscription = presenter.status
|
statusSubscription = presenter.status
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe {
|
.subscribe {
|
||||||
when(it) {
|
when (it) {
|
||||||
is InterceptResult.Success -> {
|
is InterceptResult.Success -> {
|
||||||
intercept_progress.gone()
|
intercept_progress.gone()
|
||||||
intercept_status.text = "Launching app..."
|
intercept_status.text = "Launching app..."
|
||||||
|
@ -3,8 +3,8 @@ package exh.ui.intercept
|
|||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import exh.GalleryAddEvent
|
import exh.GalleryAddEvent
|
||||||
import exh.GalleryAdder
|
import exh.GalleryAdder
|
||||||
import rx.subjects.BehaviorSubject
|
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
import rx.subjects.BehaviorSubject
|
||||||
|
|
||||||
class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
|
class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
|
||||||
private val galleryAdder = GalleryAdder()
|
private val galleryAdder = GalleryAdder()
|
||||||
@ -13,11 +13,11 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun loadGallery(gallery: String) {
|
fun loadGallery(gallery: String) {
|
||||||
//Do not load gallery if already loading
|
// Do not load gallery if already loading
|
||||||
if(status.value is InterceptResult.Idle) {
|
if (status.value is InterceptResult.Idle) {
|
||||||
status.onNext(InterceptResult.Loading())
|
status.onNext(InterceptResult.Loading())
|
||||||
|
|
||||||
//Load gallery async
|
// Load gallery async
|
||||||
thread {
|
thread {
|
||||||
val result = galleryAdder.addGallery(gallery)
|
val result = galleryAdder.addGallery(gallery)
|
||||||
|
|
||||||
@ -35,6 +35,6 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
|
|||||||
sealed class InterceptResult {
|
sealed class InterceptResult {
|
||||||
class Idle : InterceptResult()
|
class Idle : InterceptResult()
|
||||||
class Loading : InterceptResult()
|
class Loading : InterceptResult()
|
||||||
data class Success(val mangaId: Long): InterceptResult()
|
data class Success(val mangaId: Long) : InterceptResult()
|
||||||
data class Failure(val reason: String): InterceptResult()
|
data class Failure(val reason: String) : InterceptResult()
|
||||||
}
|
}
|
||||||
|
@ -28,21 +28,21 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
val prefs: PreferencesHelper by injectLazy()
|
val prefs: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
val fingerprintSupported
|
val fingerprintSupported
|
||||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||||
&& Reprint.isHardwarePresent()
|
Reprint.isHardwarePresent() &&
|
||||||
&& Reprint.hasFingerprintRegistered()
|
Reprint.hasFingerprintRegistered()
|
||||||
|
|
||||||
val useFingerprint
|
val useFingerprint
|
||||||
get() = fingerprintSupported
|
get() = fingerprintSupported &&
|
||||||
&& prefs.eh_lockUseFingerprint().getOrDefault()
|
prefs.eh_lockUseFingerprint().getOrDefault()
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
override fun onAttached() {
|
override fun onAttached() {
|
||||||
super.onAttached()
|
super.onAttached()
|
||||||
if(fingerprintSupported) {
|
if (fingerprintSupported) {
|
||||||
updateSummary()
|
updateSummary()
|
||||||
onChange {
|
onChange {
|
||||||
if(it as Boolean)
|
if (it as Boolean)
|
||||||
tryChange()
|
tryChange()
|
||||||
else
|
else
|
||||||
prefs.eh_lockUseFingerprint().set(false)
|
prefs.eh_lockUseFingerprint().set(false)
|
||||||
@ -51,7 +51,7 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
} else {
|
} else {
|
||||||
title = "Fingerprint unsupported"
|
title = "Fingerprint unsupported"
|
||||||
shouldDisableView = true
|
shouldDisableView = true
|
||||||
summary = if(!Reprint.hasFingerprintRegistered())
|
summary = if (!Reprint.hasFingerprintRegistered())
|
||||||
"No fingerprints enrolled!"
|
"No fingerprints enrolled!"
|
||||||
else
|
else
|
||||||
"Fingerprint unlock is unsupported on this device!"
|
"Fingerprint unlock is unsupported on this device!"
|
||||||
@ -61,7 +61,7 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
|
|
||||||
private fun updateSummary() {
|
private fun updateSummary() {
|
||||||
isChecked = useFingerprint
|
isChecked = useFingerprint
|
||||||
title = if(isChecked)
|
title = if (isChecked)
|
||||||
"Fingerprint enabled"
|
"Fingerprint enabled"
|
||||||
else
|
else
|
||||||
"Fingerprint disabled"
|
"Fingerprint disabled"
|
||||||
@ -146,4 +146,4 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
subscription.unsubscribe()
|
subscription.unsubscribe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package exh.ui.lock
|
package exh.ui.lock
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.bluelinelabs.conductor.Router
|
import com.bluelinelabs.conductor.Router
|
||||||
@ -8,7 +7,6 @@ import com.bluelinelabs.conductor.RouterTransaction
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
object LockActivityDelegate {
|
object LockActivityDelegate {
|
||||||
private val preferences by injectLazy<PreferencesHelper>()
|
private val preferences by injectLazy<PreferencesHelper>()
|
||||||
@ -20,7 +18,6 @@ object LockActivityDelegate {
|
|||||||
.popChangeHandler(LockChangeHandler(animate)))
|
.popChangeHandler(LockChangeHandler(animate)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun onCreate(activity: FragmentActivity) {
|
fun onCreate(activity: FragmentActivity) {
|
||||||
preferences.secureScreen().asObservable()
|
preferences.secureScreen().asObservable()
|
||||||
.subscribe {
|
.subscribe {
|
||||||
@ -42,5 +39,4 @@ object LockActivityDelegate {
|
|||||||
private fun isAppLocked(router: Router): Boolean {
|
private fun isAppLocked(router: Router): Boolean {
|
||||||
return router.backstack.lastOrNull()?.controller() is LockController
|
return router.backstack.lastOrNull()?.controller() is LockController
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
|
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
|
||||||
import java.util.*
|
import java.util.ArrayList
|
||||||
|
|
||||||
class LockChangeHandler : AnimatorChangeHandler {
|
class LockChangeHandler : AnimatorChangeHandler {
|
||||||
constructor(): super()
|
constructor() : super()
|
||||||
|
|
||||||
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
|
||||||
|
|
||||||
@ -36,6 +36,4 @@ class LockChangeHandler : AnimatorChangeHandler {
|
|||||||
|
|
||||||
override fun copy(): ControllerChangeHandler =
|
override fun copy(): ControllerChangeHandler =
|
||||||
LockChangeHandler(animationDuration, removesFromViewOnPush())
|
LockChangeHandler(animationDuration, removesFromViewOnPush())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user