Optimize imports, disallow wildcard imports because of klint, run linter

This commit is contained in:
jobobby04 2020-04-04 16:30:05 -04:00 committed by Jobobby04
parent f18891a07e
commit 23ac3d18e5
138 changed files with 1192 additions and 1027 deletions

View File

@ -43,7 +43,6 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
put(COL_FLAGS, obj.flags)
val orderString = obj.mangaOrder.joinToString("/")
put(COL_MANGA_ORDER, orderString)
}
}

View File

@ -29,6 +29,4 @@ class MangaUrlPutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_URL, manga.url)
}
}

View File

@ -24,7 +24,6 @@ object CategoryTable {
$COL_MANGA_ORDER TEXT NOT NULL
)"""
val addMangaOrder: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT"
}

View File

@ -72,4 +72,4 @@ class LibraryUpdateNotifier(private val context: Context) {
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
}

View File

@ -45,4 +45,4 @@ class EmptyPreferenceDataStore : PreferenceDataStore() {
override fun putStringSet(key: String?, values: Set<String>?) {
}
}
}

View File

@ -57,4 +57,4 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
return newBody.toString().toRequestBody(requestBody.contentType())
}
}
}

View File

@ -110,4 +110,4 @@ internal class UpdaterNotifier(private val context: Context) {
}
notificationBuilder.show(Notifications.ID_UPDATER)
}
}
}

View File

@ -5,4 +5,4 @@ import androidx.preference.PreferenceScreen
interface ConfigurableSource : Source {
fun setupPreferenceScreen(screen: PreferenceScreen)
}
}

View File

@ -9,11 +9,11 @@ import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata
import kotlin.reflect.KClass
import rx.Completable
import rx.Single
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.reflect.KClass
/**
* LEWD!

View File

@ -28,4 +28,4 @@ interface UrlImportableSource : Source {
url
}
}
}
}

View File

@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.net.Uri
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.JsonObject
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.asObservableSuccess
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.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -36,23 +46,30 @@ import exh.util.UriFilter
import exh.util.UriGroup
import exh.util.ignore
import exh.util.urlImportFetchSearchManga
import java.net.URLEncoder
import java.util.ArrayList
import kotlinx.coroutines.runBlocking
import okhttp3.*
import okhttp3.CacheControl
import okhttp3.CookieJar
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode
import rx.Observable
import rx.Single
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.util.*
// TODO Consider gallery updating when doing tabbed browsing
class EHentai(override val id: Long,
val exh: Boolean,
val context: Context) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
class EHentai(
override val id: Long,
val exh: Boolean,
val context: Context
) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
override val metaClass = EHentaiSearchMetadata::class
val schema: String
@ -98,10 +115,10 @@ class EHentai(override val id: Long,
favElement?.attr("style")?.substring(14, 17)
),
manga = Manga.create(id).apply {
//Get title
// Get title
title = thumbnailElement.attr("title")
url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href"))
//Get image
// Get image
thumbnail_url = thumbnailElement.attr("src")
// TODO Parse genre + uploader + tags
@ -110,9 +127,9 @@ class EHentai(override val id: Long,
val parsedLocation = doc.location().toHttpUrlOrNull()
//Add to page if required
val hasNextPage = if (parsedLocation == null
|| !parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
// Add to page if required
val hasNextPage = if (parsedLocation == null ||
!parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) {
select("a[onclick=return false]").last()?.let {
it.text() == ">"
} ?: false
@ -212,8 +229,11 @@ class EHentai(override val id: Long,
}
}!!
private fun fetchChapterPage(chapter: SChapter, np: String,
pastUrls: List<String> = emptyList()): Observable<List<String>> {
private fun fetchChapterPage(
chapter: SChapter,
np: String,
pastUrls: List<String> = emptyList()
): Observable<List<String>> {
val urls = ArrayList(pastUrls)
return chapterPageCall(np).flatMap {
val jsoup = it.asJsoup()
@ -245,7 +265,7 @@ class EHentai(override val id: Long,
else
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) =
urlImportFetchSearchManga(query) {
searchMangaRequestObservable(page, query, filters).flatMap {
@ -377,7 +397,7 @@ class EHentai(override val id: Long,
uploader = select("#gdn").text().nullIfBlank()?.trim()
//Parse the table
// Parse the table
select("#gdd tr").forEach {
val left = it.select(".gdt1").text().nullIfBlank()?.trim()
val rightElement = it.selectFirst(".gdt2")
@ -407,13 +427,13 @@ class EHentai(override val id: Long,
}
lastUpdateCheck = System.currentTimeMillis()
if (datePosted != null
&& lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) {
if (datePosted != null &&
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) {
aged = true
XLog.d("aged %s - too old", title)
}
//Parse ratings
// Parse ratings
ignore {
averageRating = select("#rating_label")
.text()
@ -428,7 +448,7 @@ class EHentai(override val id: Long,
?.toInt()
}
//Parse tags
// Parse tags
tags.clear()
select("#taglist tr").forEach {
val namespace = it.select(".tc").text().removeSuffix(":")
@ -465,7 +485,7 @@ class EHentai(override val id: Long,
fun realImageUrlParse(response: Response, page: Page): String {
with(response.asJsoup()) {
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 {
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()
val doc = response2.asJsoup()
//Parse favorites
// Parse favorites
val parsed = extendedGenericMangaParse(doc)
result += parsed.first
//Parse fav names
// Parse fav names
if (favNames == null)
favNames = doc.select(".fp:not(.fps)").mapNotNull {
it.child(2).text()
}
//Next page
// Next page
page++
} while (parsed.second)
@ -544,7 +564,7 @@ class EHentai(override val id: Long,
fun cookiesHeader(sp: Int = spPref().getOrDefault()) = buildCookies(rawCookies(sp))
//Headers
// Headers
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())!!
fun addParam(url: String, param: String, value: String) = Uri.parse(url)
@ -565,7 +585,7 @@ class EHentai(override val id: Long,
chain.proceed(newReq)
}.build()!!
//Filters
// Filters
override fun getFilterList() = FilterList(
Watched(),
GenreGroup(),
@ -673,11 +693,11 @@ class EHentai(override val id: Long,
override fun mapUrlToMangaUrl(uri: Uri): String? {
return when (uri.pathSegments.firstOrNull()) {
"g" -> {
//Is already gallery page, do nothing
// Is already gallery page, do nothing
uri.toString()
}
"s" -> {
//Is page, fetch gallery token and use that
// Is page, fetch gallery token and use that
getGalleryUrlFromPage(uri)
}
else -> null
@ -713,7 +733,6 @@ class EHentai(override val id: Long,
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
}
companion object {
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
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 = "; ") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}
}
}

View File

@ -10,7 +10,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.network.GET
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.LewdSource
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.RaisedTag
import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
@ -32,8 +38,6 @@ import rx.Observable
import rx.Single
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.*
/**
* Man, I hate this source :(
@ -61,8 +65,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
private var tagIndexVersionCacheTime: Long = 0
private fun tagIndexVersion(): Single<Long> {
val sCachedTagIndexVersion = cachedTagIndexVersion
return if (sCachedTagIndexVersion == null
|| tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
return if (sCachedTagIndexVersion == null ||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
cachedTagIndexVersion = it
tagIndexVersionCacheTime = System.currentTimeMillis()
@ -76,8 +80,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
private var galleryIndexVersionCacheTime: Long = 0
private fun galleryIndexVersion(): Single<Long> {
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
return if (sCachedGalleryIndexVersion == null
|| galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
return if (sCachedGalleryIndexVersion == null ||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) {
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
cachedGalleryIndexVersion = it
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
* override this method.
@ -423,5 +426,4 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
SimpleDateFormat("yyyy-MM-dd HH:mm:ss'-05'", Locale.US)
}
}
}

View File

@ -2,13 +2,22 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context
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 eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.GET
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.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -30,8 +39,8 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
override val metaClass = NHentaiSearchMetadata::class
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
//TODO There is currently no way to get the most popular mangas
//TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen
// 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
return fetchLatestUpdates(page)
}
@ -39,7 +48,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
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> {
val trimmedIdQuery = query.trim().removePrefix("id:")
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
@ -246,7 +255,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
override fun getFilterList() = FilterList(SortFilter(), filterLang())
//language filtering
// language filtering
private class filterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
class SortFilter : Filter.Sort(
@ -305,7 +314,6 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
Pair("Chinese", " chinese")
)
val jsonParser by lazy {
JsonParser()
}

View File

@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.source.online.all
import android.net.Uri
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.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.ParsedHttpSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -17,14 +22,15 @@ import exh.metadata.metadata.base.RaisedTag
import exh.util.UriFilter
import exh.util.UriGroup
import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode
import rx.Observable
import java.text.SimpleDateFormat
import java.util.*
// TODO Transform into delegated source
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
//Support direct URL importing
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) {
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(
ReleaseYearRangeFilter(),
ReleaseYearYearFilter()

View File

@ -2,10 +2,14 @@ package eu.kanade.tachiyomi.source.online.english
import android.net.Uri
import com.kizitonwose.time.hours
import hu.akarnokd.rxjava.interop.RxJavaInterop
import eu.kanade.tachiyomi.network.GET
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.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -17,13 +21,18 @@ import exh.util.CachedField
import exh.util.NakedTrie
import exh.util.await
import exh.util.urlImportFetchSearchManga
import hu.akarnokd.rxjava.interop.RxJavaInterop
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.rx2.asSingle
import kotlinx.coroutines.withContext
import okhttp3.*
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element

View File

@ -4,12 +4,16 @@ import android.net.Uri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonParser
import hu.akarnokd.rxjava.interop.RxJavaInterop
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
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.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
@ -23,7 +27,9 @@ import exh.search.Text
import exh.util.await
import exh.util.dropBlank
import exh.util.urlImportFetchSearchManga
import hu.akarnokd.rxjava.interop.RxJavaInterop
import info.debatty.java.stringsimilarity.Levenshtein
import kotlin.math.ceil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
@ -36,7 +42,6 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import rx.schedulers.Schedulers
import kotlin.math.ceil
class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource {
/**
@ -182,7 +187,6 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
}
}
"/result"
} else {
"/search"

View File

@ -30,7 +30,7 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
*/
override val metaClass = HentaiCafeSearchMetadata::class
//Support direct URL importing
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters)

View File

@ -29,7 +29,7 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
*/
override val metaClass = PururinSearchMetadata::class
//Support direct URL importing
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val trimmedIdQuery = query.trim().removePrefix("id:")
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
@ -106,4 +106,4 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
override fun mapUrlToMangaUrl(uri: Uri): String? {
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
}
}
}

View File

@ -4,32 +4,28 @@ import android.net.Uri
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.asObservableSuccess
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.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.system.asJsoup
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.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.util.dropBlank
import exh.util.trimAll
import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import org.jsoup.nodes.Document
import rx.Observable
import java.text.SimpleDateFormat
import java.util.*
class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
LewdSource<TsuminoSearchMetadata, Document>, UrlImportableSource {
override val metaClass = TsuminoSearchMetadata::class;
override val metaClass = TsuminoSearchMetadata::class
override val lang = "en"
//Support direct URL importing
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters)

View File

@ -7,4 +7,4 @@ interface TabbedController {
fun configureTabs(tabs: TabLayout) {}
fun cleanupTabs(tabs: TabLayout) {}
}
}

View File

@ -8,4 +8,4 @@ abstract class BaseViewHolder(view: View) : androidx.recyclerview.widget.Recycle
override val containerView: View?
get() = itemView
}
}

View File

@ -42,4 +42,4 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
}
}
}

View File

@ -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 spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
}
}
}

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.source.filter
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible

View File

@ -5,21 +5,24 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
import exh.isLewdSource
import exh.metadata.sql.tables.SearchMetadataTable
import exh.search.SearchEngine
import exh.util.await
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.filter
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
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.

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.library
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
@ -26,7 +26,6 @@ abstract class LibraryHolder(
*/
abstract fun onSetValues(item: LibraryItem)
/**
* Called when an item is released.
*
@ -36,5 +35,4 @@ abstract class LibraryHolder(
super.onItemReleased(position)
(adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position)
}
}

View File

@ -235,4 +235,3 @@ class MangaInfoPresenter(
return toInsert
}
}

View File

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

View File

@ -17,7 +17,17 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
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 exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
@ -34,6 +44,7 @@ import exh.ui.login.LoginController
import exh.util.await
import exh.util.trans
import humanize.Humanize
import java.util.Date
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -41,7 +52,6 @@ import kotlinx.coroutines.withContext
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import java.util.*
/**
* EH Settings fragment
@ -52,17 +62,17 @@ class SettingsEhController : SettingsController() {
private val db: DatabaseHelper by injectLazy()
private fun Preference<*>.reconfigure(): Boolean {
//Listen for change commit
// Listen for change commit
asObservable()
.skip(1) //Skip first as it is emitted immediately
.take(1) //Only listen for first commit
.skip(1) // Skip first as it is emitted immediately
.take(1) // Only listen for first commit
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy {
//Only listen for first change commit
// Only listen for first change commit
WarnConfigureDialogController.uploadSettings(router)
}
//Always return true to save changes
// Always return true to save changes
return true
}

View File

@ -23,4 +23,4 @@ fun Element.attrOrText(css: String): String {
*/
fun Response.asJsoup(html: String? = null): Document {
return Jsoup.parse(html ?: body!!.string(), request.url.toString())
}
}

View File

@ -16,4 +16,4 @@ fun ImageView.setVectorCompat(@DrawableRes drawable: Int, tint: Int? = null) {
vector?.setTint(tint)
}
setImageDrawable(vector)
}
}

View File

@ -12,4 +12,4 @@ import androidx.annotation.LayoutRes
*/
fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(layout, this, attachToRoot)
}
}

View File

@ -23,4 +23,4 @@ class IntListPreference @JvmOverloads constructor(context: Context, attrs: Attri
defaultReturnValue
}
}
}
}

View File

@ -53,7 +53,7 @@ private val lewdDelegatedSourceIds = SourceManager.DELEGATED_SOURCES.filter {
}.map { it.value.sourceId }.sorted()
// This method MUST be fast!
fun isLewdSource(source: Long) = source in 6900..6999
|| lewdDelegatedSourceIds.binarySearch(source) >= 0
fun isLewdSource(source: Long) = source in 6900..6999 ||
lewdDelegatedSourceIds.binarySearch(source) >= 0
fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID

View File

@ -2,6 +2,8 @@ package exh
import eu.kanade.tachiyomi.source.model.FilterList
data class EXHSavedSearch(val name: String,
val query: String,
val filterList: FilterList)
data class EXHSavedSearch(
val name: String,
val query: String,
val filterList: FilterList
)

View File

@ -16,20 +16,22 @@ class GalleryAdder {
private val sourceManager: SourceManager by injectLazy()
fun addGallery(url: String,
fav: Boolean = false,
forceSource: UrlImportableSource? = null,
throttleFunc: () -> Unit = {}): GalleryAddEvent {
fun addGallery(
url: String,
fav: Boolean = false,
forceSource: UrlImportableSource? = null,
throttleFunc: () -> Unit = {}
): GalleryAddEvent {
XLog.d("Importing gallery (url: %s, fav: %s, forceSource: %s)...", url, fav, forceSource)
try {
val uri = Uri.parse(url)
// Find matching source
val source = if(forceSource != null) {
val source = if (forceSource != null) {
try {
if (forceSource.matchesUri(uri)) forceSource
else return GalleryAddEvent.Fail.UnknownType(url)
} catch(e: Exception) {
} catch (e: Exception) {
XLog.e("Source URI match check error!", e)
return GalleryAddEvent.Fail.UnknownType(url)
}
@ -39,7 +41,7 @@ class GalleryAdder {
.find {
try {
it.matchesUri(uri)
} catch(e: Exception) {
} catch (e: Exception) {
XLog.e("Source URI match check error!", e)
false
}
@ -49,7 +51,7 @@ class GalleryAdder {
// Map URL to manga URL
val realUrl = try {
source.mapUrlToMangaUrl(uri)
} catch(e: Exception) {
} catch (e: Exception) {
XLog.e("Source URI map-to-manga error!", e)
null
} ?: return GalleryAddEvent.Fail.UnknownType(url)
@ -57,12 +59,12 @@ class GalleryAdder {
// Clean URL
val cleanedUrl = try {
source.cleanMangaUrl(realUrl)
} catch(e: Exception) {
} catch (e: Exception) {
XLog.e("Source URI clean error!", e)
null
} ?: 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()
?: Manga.create(source.id).apply {
this.url = cleanedUrl
@ -71,7 +73,7 @@ class GalleryAdder {
// Insert created manga if not in DB before 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 {
manga.id = it
}
@ -86,9 +88,9 @@ class GalleryAdder {
db.insertManga(manga).executeAsBlocking()
//Fetch and copy chapters
// Fetch and copy chapters
try {
val chapterListObs = if(source is EHentai) {
val chapterListObs = if (source is EHentai) {
source.fetchChapterList(manga, throttleFunc)
} else {
source.fetchChapterList(manga)
@ -102,10 +104,10 @@ class GalleryAdder {
}
return GalleryAddEvent.Success(url, manga)
} catch(e: Exception) {
} catch (e: Exception) {
XLog.w("Could not add gallery (url: $url)!", e)
if(e is EHentai.GalleryNotFoundException) {
if (e is EHentai.GalleryNotFoundException) {
return GalleryAddEvent.Fail.NotFound(url)
}
@ -120,21 +122,25 @@ sealed class GalleryAddEvent {
abstract val galleryUrl: String
open val galleryTitle: String? = null
class Success(override val galleryUrl: String,
val manga: Manga): GalleryAddEvent() {
class Success(
override val galleryUrl: String,
val manga: Manga
) : GalleryAddEvent() {
override val galleryTitle = manga.title
override val logMessage = "Added gallery: $galleryTitle"
}
sealed class Fail: GalleryAddEvent() {
class UnknownType(override val galleryUrl: String): Fail() {
sealed class Fail : GalleryAddEvent() {
class UnknownType(override val galleryUrl: String) : Fail() {
override val logMessage = "Unknown gallery type for gallery: $galleryUrl"
}
open class Error(override val galleryUrl: String,
override val logMessage: String): Fail()
open class Error(
override val galleryUrl: String,
override val logMessage: String
) : Fail()
class NotFound(galleryUrl: String):
class NotFound(galleryUrl: String) :
Error(galleryUrl, "Gallery does not exist: $galleryUrl")
}
}
}

View File

@ -9,20 +9,19 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.system.jobScheduler
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.EXHMigrations
import exh.EXH_SOURCE_ID
import exh.eh.EHentaiUpdateWorker
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.await
import exh.util.cancellable
import uy.kohesive.injekt.injectLazy
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.toList
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.injectLazy
object DebugFunctions {
val app: Application by injectLazy()
@ -31,7 +30,7 @@ object DebugFunctions {
val sourceManager: SourceManager by injectLazy()
fun forceUpgradeMigration() {
prefs.eh_lastVersionCode().set(0)
prefs.eh_lastVersionCode().set(0)
EXHMigrations.upgrade(prefs)
}
@ -47,7 +46,7 @@ object DebugFunctions {
for (manga in allManga) {
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
if(meta != null) {
if (meta != null) {
// remove age flag
meta.aged = false
db.insertFlatMetadata(meta.flatten()).await()

View File

@ -26,4 +26,4 @@ enum class DebugToggles(val default: Boolean) {
companion object {
private val prefs: PreferencesHelper by injectLazy()
}
}
}

View File

@ -8,8 +8,12 @@ import android.widget.HorizontalScrollView
import android.widget.TextView
import androidx.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.ui.setting.*
import eu.kanade.tachiyomi.util.preference.*
import eu.kanade.tachiyomi.ui.setting.SettingsController
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.full.declaredFunctions
@ -41,7 +45,7 @@ class SettingsDebugController : SettingsController() {
view.text = "Function returned result:\n\n$result"
MaterialDialog.Builder(context)
.customView(hView, true)
} catch(t: Throwable) {
} catch (t: Throwable) {
view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}"
MaterialDialog.Builder(context)
.customView(hView, true)
@ -59,8 +63,8 @@ class SettingsDebugController : SettingsController() {
title = it.name.replace('_', ' ').toLowerCase().capitalize()
key = it.prefKey
defaultValue = it.default
summaryOn = if(it.default) "" else MODIFIED_TEXT
summaryOff = if(it.default) MODIFIED_TEXT else ""
summaryOn = if (it.default) "" else MODIFIED_TEXT
summaryOff = if (it.default) MODIFIED_TEXT else ""
}
}
}
@ -74,4 +78,4 @@ class SettingsDebugController : SettingsController() {
companion object {
private val MODIFIED_TEXT = Html.fromHtml("<font color='red'>MODIFIED</font>")
}
}
}

View File

@ -1,19 +1,21 @@
package exh.eh
class EHentaiThrottleManager(private val max: Int = THROTTLE_MAX,
private val inc: Int = THROTTLE_INC) {
class EHentaiThrottleManager(
private val max: Int = THROTTLE_MAX,
private val inc: Int = THROTTLE_INC
) {
private var lastThrottleTime: Long = 0
var throttleTime: Long = 0
private set
fun throttle() {
//Throttle requests if necessary
// Throttle requests if necessary
val now = System.currentTimeMillis()
val timeDiff = now - lastThrottleTime
if(timeDiff < throttleTime)
if (timeDiff < throttleTime)
Thread.sleep(throttleTime - timeDiff)
if(throttleTime < max)
if (throttleTime < max)
throttleTime += inc
lastThrottleTime = System.currentTimeMillis()
@ -28,4 +30,4 @@ class EHentaiThrottleManager(private val max: Int = THROTTLE_MAX,
const val THROTTLE_MAX = 5500
const val THROTTLE_INC = 20
}
}
}

View File

@ -8,10 +8,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import java.io.File
import rx.Observable
import rx.Single
import uy.kohesive.injekt.injectLazy
import java.io.File
data class ChapterChain(val manga: Manga, val chapters: List<Chapter>)
@ -61,7 +61,7 @@ class EHentaiUpdateHelper(context: Context) {
val chainsAsChapters = chains.flatMap { it.chapters }
if(toDiscard.isNotEmpty()) {
if (toDiscard.isNotEmpty()) {
var new = false
// Copy chain chapters to curChapters
@ -75,9 +75,9 @@ class EHentaiUpdateHelper(context: Context) {
chain.chapters.map { chapter ->
// Convert old style chapters to new style chapters if possible
if(chapter.date_upload <= 0
&& meta?.datePosted != null
&& meta?.title != null) {
if (chapter.date_upload <= 0 &&
meta?.datePosted != null &&
meta?.title != null) {
chapter.name = meta!!.title!!
chapter.date_upload = meta!!.datePosted!!
}
@ -92,7 +92,7 @@ class EHentaiUpdateHelper(context: Context) {
if (existing != null) {
existing.read = existing.read || chapter.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.bookmark = existing.bookmark || chapter.bookmark
@ -107,7 +107,7 @@ class EHentaiUpdateHelper(context: Context) {
bookmark = chapter.bookmark
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
}
@ -153,7 +153,7 @@ class EHentaiUpdateHelper(context: Context) {
}
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.
*/

View File

@ -20,8 +20,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager
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.system.jobScheduler
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.debug.DebugToggles
@ -31,18 +31,23 @@ import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.await
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.mapNotNull
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.coroutines.CoroutineContext
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class EHentaiUpdateWorker: JobService(), CoroutineScope {
class EHentaiUpdateWorker : JobService(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + Job()
@ -215,8 +220,8 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
val (acceptedRoot, discardedRoots, hasNew) =
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
if((new.isNotEmpty() && manga.id == acceptedRoot.manga.id)
|| (hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })) {
if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) ||
(hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })) {
updatedManga += acceptedRoot.manga
}
@ -235,7 +240,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
)
)
if(updatedManga.isNotEmpty()) {
if (updatedManga.isNotEmpty()) {
updateNotifier.showResultNotification(updatedManga)
}
}
@ -254,10 +259,10 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
val newChapters = source.fetchChapterList(manga).toSingle().await(Schedulers.io())
val (new, _) = syncChaptersWithSource(db, newChapters, manga, source) // Not suspending, but does block, maybe fix this?
return new to db.getChapters(manga).await()
} catch(t: Throwable) {
if(t is EHentai.GalleryNotFoundException) {
} catch (t: Throwable) {
if (t is EHentai.GalleryNotFoundException) {
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
if(meta != null) {
if (meta != null) {
// Age dead galleries
logger.d("Aged %s - notfound", manga.id)
meta.aged = true
@ -286,18 +291,20 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
private fun Context.baseBackgroundJobInfo(isTest: Boolean): 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())
}
private fun Context.periodicBackgroundJobInfo(period: Long,
requireCharging: Boolean,
requireUnmetered: Boolean): JobInfo {
private fun Context.periodicBackgroundJobInfo(
period: Long,
requireCharging: Boolean,
requireUnmetered: Boolean
): JobInfo {
return baseBackgroundJobInfo(false)
.setPeriodic(period)
.setPersisted(true)
.setRequiredNetworkType(
if(requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
else JobInfo.NETWORK_TYPE_ANY)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -321,7 +328,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
fun launchBackgroundTest(context: Context) {
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!")
} else {
logger.d("Successfully scheduled background test job!")
@ -344,7 +351,7 @@ class EHentaiUpdateWorker: JobService(), CoroutineScope {
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!")
} else {
logger.d("Successfully scheduled background update job!")

View File

@ -1,7 +1,7 @@
package exh.eh
data class EHentaiUpdaterStats(
val startTime: Long,
val possibleUpdates: Int,
val updateCount: Int
)
val startTime: Long,
val possibleUpdates: Int,
val updateCount: Int
)

View File

@ -1,3 +1,3 @@
package exh.eh
class GalleryNotUpdatedException(val network: Boolean, cause: Throwable): RuntimeException(cause)
class GalleryNotUpdatedException(val network: Boolean, cause: Throwable) : RuntimeException(cause)

View File

@ -3,9 +3,6 @@ package exh.eh
import android.util.SparseArray
import androidx.core.util.AtomicFile
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.File
import java.io.FileNotFoundException
@ -13,6 +10,18 @@ import java.io.InputStream
import java.nio.ByteBuffer
import kotlin.concurrent.thread
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
@ -23,9 +32,9 @@ import kotlin.coroutines.CoroutineContext
* @author nulldev
*/
class MemAutoFlushingLookupTable<T>(
file: File,
private val serializer: EntrySerializer<T>,
private val debounceTimeMs: Long = 3000
file: File,
private val serializer: EntrySerializer<T>,
private val debounceTimeMs: Long = 3000
) : CoroutineScope, Closeable {
/**
* The context of this scope.
@ -49,7 +58,7 @@ class MemAutoFlushingLookupTable<T>(
private val atomicFile = AtomicFile(file)
private val shutdownHook = thread(start = false) {
if(!flushed) writeSynchronously()
if (!flushed) writeSynchronously()
}
init {
@ -62,9 +71,9 @@ class MemAutoFlushingLookupTable<T>(
var readIter = 0
while (true) {
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
if(readIter == byteCount) return true
if (readIter == byteCount) return true
}
}
@ -74,16 +83,16 @@ class MemAutoFlushingLookupTable<T>(
atomicFile.openRead().buffered().use { input ->
val bb = ByteBuffer.allocate(8)
while(true) {
if(!input.requireBytes(bb.array(), 8)) break
while (true) {
if (!input.requireBytes(bb.array(), 8)) break
val k = bb.getInt(0)
val size = bb.getInt(4)
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)))
}
}
} catch(e: FileNotFoundException) {
} catch (e: FileNotFoundException) {
XLog.d("Lookup table not found!", e)
// Ignored
}
@ -97,11 +106,11 @@ class MemAutoFlushingLookupTable<T>(
flushed = false
launch {
delay(debounceTimeMs)
if(id != writeCounter) return@launch
if (id != writeCounter) return@launch
mutex.withLock {
// Second check inside of mutex to prevent dupe writes
if(id != writeCounter) return@launch
if (id != writeCounter) return@launch
withContext(NonCancellable) {
writeSynchronously()
@ -118,7 +127,7 @@ class MemAutoFlushingLookupTable<T>(
val fos = atomicFile.startWrite()
try {
val out = fos.buffered()
for(i in 0 until table.size()) {
for (i in 0 until table.size()) {
val k = table.keyAt(i)
val v = serializer.write(table.valueAt(i)).toByteArray(Charsets.UTF_8)
bb.putInt(0, k)
@ -128,7 +137,7 @@ class MemAutoFlushingLookupTable<T>(
}
out.flush()
atomicFile.finishWrite(fos)
} catch(t: Throwable) {
} catch (t: Throwable) {
atomicFile.failWrite(fos)
throw t
}
@ -212,4 +221,4 @@ class MemAutoFlushingLookupTable<T>(
private const val INITIAL_SIZE = 1000
private const val ENTRY_SIZE_BYTES = 8
}
}
}

View File

@ -5,7 +5,7 @@ import io.realm.RealmObject
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import java.util.*
import java.util.UUID
@RealmClass
open class FavoriteEntry : RealmObject() {

View File

@ -17,18 +17,21 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast
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.EHentaiUpdateWorker
import exh.util.ignore
import exh.util.trans
import kotlin.concurrent.thread
import okhttp3.FormBody
import okhttp3.Request
import rx.subjects.BehaviorSubject
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.concurrent.thread
class FavoritesSyncHelper(val context: Context) {
private val db: DatabaseHelper by injectLazy()
@ -55,7 +58,7 @@ class FavoritesSyncHelper(val context: Context) {
@Synchronized
fun runSync() {
if(status.value !is FavoritesSyncStatus.Idle) {
if (status.value !is FavoritesSyncStatus.Idle) {
return
}
@ -65,8 +68,8 @@ class FavoritesSyncHelper(val context: Context) {
}
private fun beginSync() {
//Check if logged in
if(!prefs.enableExhentai().getOrDefault()) {
// Check if logged in
if (!prefs.enableExhentai().getOrDefault()) {
status.onNext(FavoritesSyncStatus.Error("Please log in!"))
return
}
@ -76,9 +79,9 @@ class FavoritesSyncHelper(val context: Context) {
val libraryManga = db.getLibraryMangas().executeAsBlocking()
val seenManga = HashSet<Long>(libraryManga.size)
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()
status.onNext(FavoritesSyncStatus.BadLibraryState
.MangaInMultipleCategories(it, inCategories))
@ -89,20 +92,20 @@ class FavoritesSyncHelper(val context: Context) {
}
}
//Download remote favorites
// Download remote favorites
val favorites = try {
status.onNext(FavoritesSyncStatus.Processing("Downloading favorites from remote server"))
exh.fetchFavorites()
} catch(e: Exception) {
} catch (e: Exception) {
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
}
val errorList = mutableListOf<String>()
try {
//Take wake + wifi locks
// Take wake + wifi locks
ignore { wakeLock?.release() }
wakeLock = ignore {
context.powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
@ -124,20 +127,20 @@ class FavoritesSyncHelper(val context: Context) {
db.inTransaction {
status.onNext(FavoritesSyncStatus.Processing("Calculating remote changes"))
val remoteChanges = storage.getChangedRemoteEntries(realm, favorites.first)
val localChanges = if(prefs.eh_readOnlySync().getOrDefault()) {
null //Do not build local changes if they are not going to be applied
val localChanges = if (prefs.eh_readOnlySync().getOrDefault()) {
null // Do not build local changes if they are not going to be applied
} else {
status.onNext(FavoritesSyncStatus.Processing("Calculating local changes"))
storage.getChangedDbEntries(realm)
}
//Apply remote categories
// Apply remote categories
status.onNext(FavoritesSyncStatus.Processing("Updating category names"))
applyRemoteCategories(errorList, favorites.second)
//Apply change sets
// Apply change sets
applyChangeSetToLocal(errorList, remoteChanges)
if(localChanges != null)
if (localChanges != null)
applyChangeSetToRemote(errorList, localChanges)
status.onNext(FavoritesSyncStatus.Processing("Cleaning up"))
@ -150,16 +153,16 @@ class FavoritesSyncHelper(val context: Context) {
launchUI {
theContext.toast("Sync complete!")
}
} catch(e: IgnoredException) {
//Do not display error as this error has already been reported
logger.w( "Ignoring exception!", e)
} catch (e: IgnoredException) {
// Do not display error as this error has already been reported
logger.w("Ignoring exception!", e)
return
} catch (e: Exception) {
status.onNext(FavoritesSyncStatus.Error("Unknown error: ${e.message}"))
logger.e( "Sync error!", e)
logger.e("Sync error!", e)
return
} finally {
//Release wake + wifi locks
// Release wake + wifi locks
ignore {
wakeLock?.release()
wakeLock = null
@ -175,7 +178,7 @@ class FavoritesSyncHelper(val context: Context) {
}
}
if(errorList.isEmpty())
if (errorList.isEmpty())
status.onNext(FavoritesSyncStatus.Idle())
else
status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList))
@ -195,31 +198,31 @@ class FavoritesSyncHelper(val context: Context) {
Category.create(remote).apply {
order = index
//Going through categories list from front to back
//If category does not exist, list size <= category index
//Thus, we can just add it here and not worry about indexing
// Going through categories list from front to back
// If category does not exist, list size <= category index
// Thus, we can just add it here and not worry about indexing
newLocalCategories += this
}
}
if(local.name != remote) {
if (local.name != remote) {
changed = true
local.name = remote
}
}
//Ensure consistent ordering
// Ensure consistent ordering
newLocalCategories.forEachIndexed { index, category ->
if(category.order != index) {
if (category.order != index) {
changed = true
category.order = index
}
}
//Only insert categories if changed
if(changed)
// Only insert categories if changed
if (changed)
db.insertCategories(newLocalCategories).executeAsBlocking()
}
@ -236,10 +239,10 @@ class FavoritesSyncHelper(val context: Context) {
.build())
.build()
if(!explicitlyRetryExhRequest(10, request)) {
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
if (!explicitlyRetryExhRequest(10, request)) {
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
} else {
status.onNext(FavoritesSyncStatus.Error(errorString))
@ -251,7 +254,7 @@ class FavoritesSyncHelper(val context: Context) {
private fun explicitlyRetryExhRequest(retryCount: Int, request: Request): Boolean {
var success = false
for(i in 1 .. retryCount) {
for (i in 1..retryCount) {
try {
val resp = exh.client.newCall(request).execute()
@ -260,7 +263,7 @@ class FavoritesSyncHelper(val context: Context) {
break
}
} 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) {
//Apply removals
if(changeSet.removed.isNotEmpty()) {
// Apply removals
if (changeSet.removed.isNotEmpty()) {
status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server"))
val formBody = FormBody.Builder()
.add("ddact", "delete")
.add("apply", "Apply")
//Add change set to form
// Add change set to form
changeSet.removed.forEach {
formBody.add("modifygids[]", it.gid)
}
@ -286,10 +289,10 @@ class FavoritesSyncHelper(val context: Context) {
.post(formBody.build())
.build()
if(!explicitlyRetryExhRequest(10, request)) {
if (!explicitlyRetryExhRequest(10, request)) {
val errorString = "Unable to delete galleries from the remote servers!"
if(prefs.eh_lenientSync().getOrDefault()) {
if (prefs.eh_lenientSync().getOrDefault()) {
errorList += errorString
} else {
status.onNext(FavoritesSyncStatus.Error(errorString))
@ -298,7 +301,7 @@ class FavoritesSyncHelper(val context: Context) {
}
}
//Apply additions
// Apply additions
throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it ->
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) {
val removedManga = mutableListOf<Manga>()
//Apply removals
// Apply removals
changeSet.removed.forEachIndexed { index, it ->
status.onNext(FavoritesSyncStatus.Processing("Removing gallery ${index + 1} of ${changeSet.removed.size} from local library"))
val url = it.getUrl()
//Consider both EX and EH sources
// Consider both EX and EH sources
listOf(db.getManga(url, EXH_SOURCE_ID),
db.getManga(url, EH_SOURCE_ID)).forEach {
val manga = it.executeAsBlocking()
if(manga?.favorite == true) {
if (manga?.favorite == true) {
manga.favorite = false
db.updateMangaFavorite(manga).executeAsBlocking()
removedManga += manga
@ -339,7 +342,7 @@ class FavoritesSyncHelper(val context: Context) {
val insertedMangaCategories = mutableListOf<Pair<MangaCategory, Manga>>()
val categories = db.getCategories().executeAsBlocking()
//Apply additions
// Apply additions
throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it ->
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()
//Import using gallery adder
// Import using gallery adder
val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}",
true,
exh,
throttleManager::throttle)
if(result is GalleryAddEvent.Fail) {
if(result is GalleryAddEvent.Fail.NotFound) {
if (result is GalleryAddEvent.Fail) {
if (result is GalleryAddEvent.Fail.NotFound) {
XLog.e("Remote gallery does not exist, skipping: %s!", it.getUrl())
// Skip this gallery, it no longer exists
return@forEachIndexed
@ -365,13 +368,13 @@ class FavoritesSyncHelper(val context: Context) {
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
} else {
status.onNext(FavoritesSyncStatus.Error(errorString))
throw IgnoredException()
}
} else if(result is GalleryAddEvent.Success) {
} else if (result is GalleryAddEvent.Success) {
insertedMangaCategories += MangaCategory.create(result.manga,
categories[it.category]) to result.manga
}
@ -385,8 +388,8 @@ class FavoritesSyncHelper(val context: Context) {
}
}
fun needWarnThrottle()
= throttleManager.throttleTime >= THROTTLE_WARN
fun needWarnThrottle() =
throttleManager.throttleTime >= THROTTLE_WARN
class IgnoredException : RuntimeException()
@ -399,12 +402,14 @@ sealed class FavoritesSyncStatus(val message: String) {
class Error(message: String) : FavoritesSyncStatus(message)
class Idle : FavoritesSyncStatus("Waiting for sync to start")
sealed class BadLibraryState(message: String) : FavoritesSyncStatus(message) {
class MangaInMultipleCategories(val manga: Manga,
val categories: List<Category>):
class MangaInMultipleCategories(
val manga: Manga,
val categories: List<Category>
) :
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
}
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."
else
message)

View File

@ -20,8 +20,8 @@ class LocalFavoritesStorage {
fun getRealm() = Realm.getInstance(realmConfig)
fun getChangedDbEntries(realm: Realm)
= getChangedEntries(realm,
fun getChangedDbEntries(realm: Realm) =
getChangedEntries(realm,
parseToFavoriteEntries(
loadDbCategories(
db.getFavoriteMangas()
@ -31,8 +31,8 @@ class LocalFavoritesStorage {
)
)
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>)
= getChangedEntries(realm,
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) =
getChangedEntries(realm,
parseToFavoriteEntries(
entries.asSequence().map {
Pair(it.fav, it.manga.apply {
@ -51,10 +51,10 @@ class LocalFavoritesStorage {
)
)
//Delete old snapshot
// Delete old snapshot
realm.delete(FavoriteEntry::class.java)
//Insert new snapshots
// Insert new snapshots
realm.copyToRealm(dbMangas.toList())
}
@ -80,18 +80,18 @@ class LocalFavoritesStorage {
return ChangeSet(added, removed)
}
private fun Realm.queryRealmForEntry(entry: FavoriteEntry)
= where(FavoriteEntry::class.java)
private fun Realm.queryRealmForEntry(entry: FavoriteEntry) =
where(FavoriteEntry::class.java)
.equalTo(FavoriteEntry::gid.name, entry.gid)
.equalTo(FavoriteEntry::token.name, entry.token)
.equalTo(FavoriteEntry::category.name, entry.category)
.findFirst()
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry)
= list.find {
it.gid == entry.gid
&& it.token == entry.token
&& it.category == entry.category
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) =
list.find {
it.gid == entry.gid &&
it.token == entry.token &&
it.category == entry.category
}
private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> {
@ -105,8 +105,8 @@ class LocalFavoritesStorage {
}
}
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>)
= manga.filter {
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) =
manga.filter {
validateDbManga(it.second)
}.mapNotNull {
FavoriteEntry().apply {
@ -115,18 +115,20 @@ class LocalFavoritesStorage {
token = EHentaiSearchMetadata.galleryToken(it.second.url)
category = it.first
if(this.category > MAX_CATEGORIES)
if (this.category > MAX_CATEGORIES)
return@mapNotNull null
}
}
private fun validateDbManga(manga: Manga)
= manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
private fun validateDbManga(manga: Manga) =
manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
companion object {
const val MAX_CATEGORIES = 9
}
}
data class ChangeSet(val added: List<FavoriteEntry>,
val removed: List<FavoriteEntry>)
data class ChangeSet(
val added: List<FavoriteEntry>,
val removed: List<FavoriteEntry>
)

View File

@ -4,42 +4,46 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess
import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
import java.security.MessageDigest
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import org.vepta.vdm.ByteCursor
import rx.Observable
import rx.Single
import java.security.MessageDigest
private typealias HashedTerm = ByteArray
private data class DataPair(val offset: Long, val length: Int)
private data class Node(val keys: List<ByteArray>,
val datas: List<DataPair>,
val subnodeAddresses: List<Long>)
private data class Node(
val keys: List<ByteArray>,
val datas: List<DataPair>,
val subnodeAddresses: List<Long>
)
/**
* Kotlin port of the hitomi.la search algorithm
* @author NerdNumber9
*/
class HitomiNozomi(private val client: OkHttpClient,
private val tagIndexVersion: Long,
private val galleriesIndexVersion: Long) {
class HitomiNozomi(
private val client: OkHttpClient,
private val tagIndexVersion: Long,
private val galleriesIndexVersion: Long
) {
fun getGalleryIdsForQuery(query: String): Single<List<Int>> {
val replacedQuery = query.replace('_', ' ')
if(':' in replacedQuery) {
if (':' in replacedQuery) {
val sides = replacedQuery.split(':')
val namespace = sides[0]
var tag = sides[1]
var area: String? = namespace
var language = "all"
if(namespace == "female" || namespace == "male") {
if (namespace == "female" || namespace == "male") {
area = "tag"
tag = replacedQuery
} else if(namespace == "language") {
} else if (namespace == "language") {
area = null
language = tag
tag = "index"
@ -52,7 +56,7 @@ class HitomiNozomi(private val client: OkHttpClient,
val field = "galleries"
return getNodeAtAddress(field, 0).flatMap { node ->
if(node == null) {
if (node == null) {
Single.just(null)
} else {
BSearch(field, key, node).flatMap { data ->
@ -67,12 +71,12 @@ class HitomiNozomi(private val client: OkHttpClient,
}
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
if(data == null)
if (data == null)
return Single.just(emptyList())
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
val (offset, length) = data
if(length > 100000000 || length <= 0)
if (length > 100000000 || length <= 0)
return Single.just(emptyList())
return client.newCall(rangedGet(url, offset, offset + length - 1))
@ -82,7 +86,7 @@ class HitomiNozomi(private val client: OkHttpClient,
}
.onErrorReturn { ByteArray(0) }
.map { inbuf ->
if(inbuf.isEmpty())
if (inbuf.isEmpty())
return@map emptyList<Int>()
val view = ByteCursor(inbuf)
@ -90,13 +94,13 @@ class HitomiNozomi(private val client: OkHttpClient,
val expectedLength = numberOfGalleryIds * 4 + 4
if(numberOfGalleryIds > 10000000
|| numberOfGalleryIds <= 0
|| inbuf.size != expectedLength) {
if (numberOfGalleryIds > 10000000 ||
numberOfGalleryIds <= 0 ||
inbuf.size != expectedLength) {
return@map emptyList<Int>()
}
(1 .. numberOfGalleryIds).map {
(1..numberOfGalleryIds).map {
view.nextInt()
}
}.toSingle()
@ -105,12 +109,12 @@ class HitomiNozomi(private val client: OkHttpClient,
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
fun compareByteArrays(dv1: ByteArray, dv2: ByteArray): Int {
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 dv2i = dv2[i].toInt() and 0xFF
if(dv1i < dv2i)
if (dv1i < dv2i)
return -1
else if(dv1i > dv2i)
else if (dv1i > dv2i)
return 1
}
return 0
@ -119,9 +123,9 @@ class HitomiNozomi(private val client: OkHttpClient,
fun locateKey(key: ByteArray, node: Node): Pair<Boolean, Int> {
var cmpResult = -1
var lastI = 0
for(nodeKey in node.keys) {
for (nodeKey in node.keys) {
cmpResult = compareByteArrays(key, nodeKey)
if(cmpResult <= 0) break
if (cmpResult <= 0) break
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)
}
val (there, where) = locateKey(key, node)
if(there) {
if (there) {
return Single.just(node.datas[where])
} else if(isLeaf(node)) {
} else if (isLeaf(node)) {
return Single.just(null)
}
@ -154,20 +158,20 @@ class HitomiNozomi(private val client: OkHttpClient,
val numberOfKeys = view.nextInt()
val keys = (1 .. numberOfKeys).map {
val keys = (1..numberOfKeys).map {
val keySize = view.nextInt()
view.next(keySize)
}
val numberOfDatas = view.nextInt()
val datas = (1 .. numberOfDatas).map {
val datas = (1..numberOfDatas).map {
val offset = view.nextLong()
val length = view.nextInt()
DataPair(offset, length)
}
val numberOfSubnodeAddresses = B + 1
val subnodeAddresses = (1 .. numberOfSubnodeAddresses).map {
val subnodeAddresses = (1..numberOfSubnodeAddresses).map {
view.nextLong()
}
@ -176,7 +180,7 @@ class HitomiNozomi(private val client: OkHttpClient,
private fun getNodeAtAddress(field: String, address: Long): Single<Node?> {
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"
}
@ -187,7 +191,7 @@ class HitomiNozomi(private val client: OkHttpClient,
}
.onErrorReturn { ByteArray(0) }
.map { nodedata ->
if(nodedata.isNotEmpty()) {
if (nodedata.isNotEmpty()) {
decodeNode(nodedata)
} else null
}.toSingle()
@ -195,7 +199,7 @@ class HitomiNozomi(private val client: OkHttpClient,
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> {
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"
}
@ -206,7 +210,7 @@ class HitomiNozomi(private val client: OkHttpClient,
.map { resp ->
val body = resp.body!!.bytes()
val cursor = ByteCursor(body)
(1 .. body.size / 4).map {
(1..body.size / 4).map {
cursor.nextInt()
}
}.toSingle()
@ -234,11 +238,10 @@ class HitomiNozomi(private val client: OkHttpClient,
.build())
}
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
.asObservableSuccess()
.map { it.body!!.string().toLong() }
}
}
}
}

View File

@ -9,17 +9,17 @@ class CrashlyticsPrinter(private val logLevel: Int) : Printer {
* Print log in new line.
*
* @param logLevel the level of log
* @param tag the tag of log
* @param msg the msg of log
* @param tag the tag of log
* @param msg the msg of log
*/
override fun println(logLevel: Int, tag: String?, msg: String?) {
if(logLevel >= this.logLevel) {
if (logLevel >= this.logLevel) {
try {
Crashlytics.log(logLevel, tag, msg)
} catch (t: Throwable) {
// Crash in debug if shit like this happens
if(BuildConfig.DEBUG) throw t
if (BuildConfig.DEBUG) throw t
}
}
}
}
}

View File

@ -4,11 +4,11 @@ import android.content.Context
import android.text.Html
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import com.ms_square.debugoverlay.DataObserver
import com.ms_square.debugoverlay.OverlayModule
import eu.kanade.tachiyomi.BuildConfig
import android.widget.LinearLayout
import android.widget.TextView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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()}
""".trimIndent()
private fun Boolean.asEnabledString() = if(this) "enabled" else "disabled"
}
private fun Boolean.asEnabledString() = if (this) "enabled" else "disabled"
}

View File

@ -23,4 +23,4 @@ enum class EHLogLevel(val description: String) {
return curLogLevel!! >= requiredLogLevel.ordinal
}
}
}
}

View File

@ -2,7 +2,7 @@ package exh.log
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)) {
val xLogger = XLog.tag("EHNetwork")
.nst()

View File

@ -1,7 +1,7 @@
package exh.metadata
import java.text.SimpleDateFormat
import java.util.*
import java.util.Locale
/**
* Metadata utils
@ -35,13 +35,12 @@ fun parseHumanReadableByteCount(arg0: String): Double? {
return null
}
fun String?.nullIfBlank(): String? = if(isNullOrBlank())
fun String?.nullIfBlank(): String? = if (isNullOrBlank())
null
else
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) }
}

View File

@ -4,12 +4,14 @@ import android.net.Uri
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.*
import exh.metadata.EX_DATE_FORMAT
import exh.metadata.ONGOING_SUFFIX
import exh.metadata.humanReadableByteCount
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.plusAssign
import java.util.Date
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
class EHentaiSearchMetadata : RaisedSearchMetadata() {
var gId: String?
@ -27,7 +29,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
var datePosted: Long? = null
var parent: String? = null
var visible: String? = null //Not a boolean
var visible: String? = null // Not a boolean
var language: String? = null
var translated: Boolean? = null
var size: Long? = null
@ -47,23 +49,23 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
}
thumbnailUrl?.let { manga.thumbnail_url = it }
//No title bug?
val titleObj = if(Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault())
// No title bug?
val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault())
altTitle ?: title
else
title
titleObj?.let { manga.title = it }
//Set artist (if we can find one)
// Set artist (if we can find one)
tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let {
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
}
//Copy tags -> genres
// Copy tags -> genres
manga.genre = tagsToGenreString()
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
//We default to completed
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
// We default to completed
manga.status = SManga.COMPLETED
title?.let { t ->
ONGOING_SUFFIX.find {
@ -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()
title?.let { titleDesc += "Title: $it\n" }
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
@ -85,7 +87,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
visible?.let { detailsDesc += "Visible: $it\n" }
language?.let {
detailsDesc += "Language: $it"
if(translated == true) detailsDesc += " TR"
if (translated == true) detailsDesc += " TR"
detailsDesc += "\n"
}
size?.let { detailsDesc += "File size: ${humanReadableByteCount(it, true)}\n" }
@ -114,10 +116,10 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
const val EH_GENRE_NAMESPACE = "genre"
private const val EH_ARTIST_NAMESPACE = "artist"
private fun splitGalleryUrl(url: String)
= url.let {
//Only parse URL if is full URL
val pathSegments = if(it.startsWith("http"))
private fun splitGalleryUrl(url: String) =
url.let {
// Only parse URL if is full URL
val pathSegments = if (it.startsWith("http"))
Uri.parse(it).pathSegments
else
it.split('/')
@ -129,10 +131,10 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
fun galleryToken(url: String) =
splitGalleryUrl(url)[2]
fun normalizeUrl(url: String)
= idAndTokenToUrl(galleryId(url), galleryToken(url))
fun normalizeUrl(url: String) =
idAndTokenToUrl(galleryId(url), galleryToken(url))
fun idAndTokenToUrl(id: String, token: String)
= "/g/$id/$token/?nw=always"
fun idAndTokenToUrl(id: String, token: String) =
"/g/$id/$token/?nw=always"
}
}
}

View File

@ -34,7 +34,6 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")
}
companion object {
@ -47,4 +46,4 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
const val TAGS_NAMESPACE = "tags"
const val ARTIST_NAMESPACE = "artist"
}
}
}

View File

@ -21,7 +21,7 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
}
// 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())
}
@ -49,4 +49,4 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
return "$BASE_URL/thumbnails/${hbid}_1.jpg#guessed"
}
}
}
}

View File

@ -27,7 +27,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
manga.artist = artist
manga.author = artist
//Not available
// Not available
manga.status = SManga.UNKNOWN
val detailsDesc = "Title: $title\n" +
@ -49,7 +49,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
const val BASE_URL = "https://hentai.cafe"
fun hcIdFromUrl(url: String)
= url.split("/").last { it.isNotBlank() }
fun hcIdFromUrl(url: String) =
url.split("/").last { it.isNotBlank() }
}
}
}

View File

@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.EX_DATE_FORMAT
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.plusAssign
import java.util.*
import java.util.Date
class HitomiSearchMetadata: RaisedSearchMetadata() {
class HitomiSearchMetadata : RaisedSearchMetadata() {
var url get() = hlId?.let { urlFromHlId(it) }
set(a) {
a?.let {
@ -62,10 +62,10 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
detailsDesc += "Language: ${it.capitalize()}\n"
}
if(series.isNotEmpty())
if (series.isNotEmpty())
detailsDesc += "Series: ${series.joinToString()}\n"
if(characters.isNotEmpty())
if (characters.isNotEmpty())
detailsDesc += "Characters: ${characters.joinToString()}\n"
uploadDate?.let {
@ -74,7 +74,7 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
manga.status = SManga.UNKNOWN
//Copy tags -> genres
// Copy tags -> genres
manga.genre = tagsToGenreString()
val tagsDesc = tagsToDescription()
@ -92,10 +92,10 @@ class HitomiSearchMetadata: RaisedSearchMetadata() {
const val LTN_BASE_URL = "https://ltn.hitomi.la"
const val BASE_URL = "https://hitomi.la"
fun hlIdFromUrl(url: String)
= url.split('/').last().split('-').last().substringBeforeLast('.')
fun hlIdFromUrl(url: String) =
url.split('/').last().split('-').last().substringBeforeLast('.')
fun urlFromHlId(id: String)
= "$BASE_URL/galleries/$id.html"
fun urlFromHlId(id: String) =
"$BASE_URL/galleries/$id.html"
}
}

View File

@ -3,12 +3,14 @@ package exh.metadata.metadata
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
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.nullIfBlank
import exh.plusAssign
import java.util.Date
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
class NHentaiSearchMetadata : RaisedSearchMetadata() {
var url get() = nhId?.let { BASE_URL + nhIdToPath(it) }
@ -39,10 +41,10 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
override fun copyTo(manga: SManga) {
nhId?.let { manga.url = nhIdToPath(it) }
if(mediaId != null) {
if (mediaId != null) {
val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault()
typeToExtension(if(hqThumbs) coverImageType else thumbnailImageType)?.let {
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if(hqThumbs)
typeToExtension(if (hqThumbs) coverImageType else thumbnailImageType)?.let {
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if (hqThumbs)
"cover"
else "thumb"}.$it"
}
@ -50,21 +52,21 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
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 {
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
}
var category: String? = null
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()
//Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
//We default to completed
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
// We default to completed
manga.status = SManga.COMPLETED
englishTitle?.let { t ->
ONGOING_SUFFIX.find {
@ -106,14 +108,14 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
fun typeToExtension(t: String?) =
when(t) {
when (t) {
"p" -> "png"
"j" -> "jpg"
else -> null
}
fun nhUrlToId(url: String)
= url.split("/").last { it.isNotBlank() }.toLong()
fun nhUrlToId(url: String) =
url.split("/").last { it.isNotBlank() }.toLong()
fun nhIdToPath(id: Long) = "/g/$id/"
}

View File

@ -41,7 +41,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
manga.title = it
titleDesc += "Title: $it\n"
}
if(altTitles.isNotEmpty())
if (altTitles.isNotEmpty())
titleDesc += "Alternate Titles: \n" + altTitles
.joinToString(separator = "\n", postfix = "\n") {
"$it"
@ -58,7 +58,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
}
status?.let {
manga.status = when(it) {
manga.status = when (it) {
"Ongoing" -> SManga.ONGOING
"Completed", "Suspended" -> SManga.COMPLETED
else -> SManga.UNKNOWN
@ -70,7 +70,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
detailsDesc += "Rating: %.2\n".format(it)
}
//Copy tags -> genres
// Copy tags -> genres
manga.genre = tagsToGenreString()
val tagsDesc = tagsToDescription()
@ -80,15 +80,14 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
.joinToString(separator = "\n")
}
companion object {
private const val TITLE_TYPE_MAIN = 0
private const val TITLE_TYPE_ALT = 1
const val TAG_TYPE_DEFAULT = 0
private fun splitGalleryUrl(url: String)
= url.let {
private fun splitGalleryUrl(url: String) =
url.let {
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
}
@ -97,13 +96,13 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
}
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),
it(PERV_EDEN_IT_SOURCE_ID);
companion object {
fun source(id: Long)
= values().find { it.id == id }
fun source(id: Long) =
values().find { it.id == id }
?: throw IllegalArgumentException("Unknown source ID: $id!")
}
}

View File

@ -47,7 +47,7 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
altTitle?.let { titleDesc += "Japanese Title: $it\n" }
val detailsDesc = StringBuilder()
(uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n"}
(uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n" }
pages?.let { detailsDesc += "Length: $it pages\n" }
fileSize?.let { detailsDesc += "Size: $it\n" }
ratingCount?.let { detailsDesc += "Rating: $averageRating ($ratingCount)\n" }
@ -69,4 +69,4 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
val BASE_URL = "https://pururin.io"
}
}
}

View File

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.EX_DATE_FORMAT
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.plusAssign
import java.util.*
import java.util.Date
class TsuminoSearchMetadata : RaisedSearchMetadata() {
var tmId: Int? = null
@ -51,15 +51,15 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
collection?.let { detailsDesc += "Collection: $it\n" }
group?.let { detailsDesc += "Group: $it\n" }
val parodiesString = parody.joinToString()
if(parodiesString.isNotEmpty()) {
if (parodiesString.isNotEmpty()) {
detailsDesc += "Parody: $parodiesString\n"
}
val charactersString = character.joinToString()
if(charactersString.isNotEmpty()) {
if (charactersString.isNotEmpty()) {
detailsDesc += "Character: $charactersString\n"
}
//Copy tags -> genres
// Copy tags -> genres
manga.genre = tagsToGenreString()
val tagsDesc = tagsToDescription()
@ -76,8 +76,8 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
val BASE_URL = "https://www.tsumino.com"
fun tmIdFromUrl(url: String)
= Uri.parse(url).lastPathSegment
fun tmIdFromUrl(url: String) =
Uri.parse(url).lastPathSegment
fun mangaUrlFromId(id: String) = "/Book/Info/$id"

View File

@ -5,19 +5,19 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import exh.metadata.sql.models.SearchMetadata
import exh.metadata.sql.models.SearchTag
import exh.metadata.sql.models.SearchTitle
import kotlin.reflect.KClass
import rx.Completable
import rx.Single
import kotlin.reflect.KClass
data class FlatMetadata(
val metadata: SearchMetadata,
val tags: List<SearchTag>,
val titles: List<SearchTitle>
val metadata: SearchMetadata,
val tags: List<SearchTag>,
val titles: List<SearchTitle>
) {
inline fun <reified T : RaisedSearchMetadata> raise(): T = raise(T::class)
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>)
= RaisedSearchMetadata.raiseFlattenGson
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>) =
RaisedSearchMetadata.raiseFlattenGson
.fromJson(metadata.extra, clazz.java).apply {
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
val single = Single.fromCallable {
val meta = getSearchMetadataForManga(mangaId).executeAsBlocking()
if(meta != null) {
if (meta != null) {
val tags = getSearchTagsForManga(mangaId).executeAsBlocking()
val titles = getSearchTitlesForManga(mangaId).executeAsBlocking()
@ -92,4 +92,4 @@ fun DatabaseHelper.insertFlatMetadata(flatMetadata: FlatMetadata) = Completable.
setSearchTagsForManga(flatMetadata.metadata.mangaId, flatMetadata.tags)
setSearchTitlesForManga(flatMetadata.metadata.mangaId, flatMetadata.titles)
}
}
}

View File

@ -30,18 +30,18 @@ abstract class RaisedSearchMetadata {
fun replaceTitleOfType(type: Int, newTitle: String?) {
titles.removeAll { it.type == type }
if(newTitle != null) titles += RaisedTitle(newTitle, type)
if (newTitle != null) titles += RaisedTitle(newTitle, type)
}
abstract fun copyTo(manga: SManga)
fun tagsToGenreString()
= tags.filter { it.type != TAG_TYPE_VIRTUAL }
.joinToString { (if(it.namespace != null) "${it.namespace}: " else "") + it.name }
fun tagsToGenreString() =
tags.filter { it.type != TAG_TYPE_VIRTUAL }
.joinToString { (if (it.namespace != null) "${it.namespace}: " else "") + it.name }
fun tagsToDescription()
= StringBuilder("Tags:\n").apply {
//BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
fun tagsToDescription() =
StringBuilder("Tags:\n").apply {
// BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
it.namespace
}.entries
@ -49,7 +49,7 @@ abstract class RaisedSearchMetadata {
groupedTags.forEach { namespace, tags ->
if (tags.isNotEmpty()) {
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
if(namespace != null) {
if (namespace != null) {
this += ""
this += namespace
this += ": "
@ -125,8 +125,8 @@ abstract class RaisedSearchMetadata {
* @param property the metadata for the property.
* @return the property value.
*/
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>)
= thisRef.getTitleOfType(type)
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) =
thisRef.getTitleOfType(type)
/**
* 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 value the value to set.
*/
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?)
= thisRef.replaceTitleOfType(type, value)
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) =
thisRef.replaceTitleOfType(type, value)
}
}
}
}

View File

@ -1,5 +1,7 @@
package exh.metadata.metadata.base
data class RaisedTag(val namespace: String?,
val name: String,
val type: Int)
data class RaisedTag(
val namespace: String?,
val name: String,
val type: Int
)

View File

@ -1,6 +1,6 @@
package exh.metadata.metadata.base
data class RaisedTitle(
val title: String,
val type: Int = 0
)
val title: String,
val type: Int = 0
)

View File

@ -2,20 +2,20 @@ package exh.metadata.sql.models
data class SearchMetadata(
// Manga ID this gallery is linked to
val mangaId: Long,
val mangaId: Long,
// Gallery uploader
val uploader: String?,
val uploader: String?,
// Extra data attached to this metadata, in JSON format
val extra: String,
val extra: String,
// 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
val extraVersion: Int
val extraVersion: Int
) {
// Transient information attached to this piece of metadata, useful for caching
var transientCache: Map<String, Any>? = null
}
}

View File

@ -2,17 +2,17 @@ package exh.metadata.sql.models
data class SearchTag(
// Tag identifier, unique
val id: Long?,
val id: Long?,
// Metadata this tag is attached to
val mangaId: Long,
val mangaId: Long,
// Tag namespace
val namespace: String?,
val namespace: String?,
// Tag name
val name: String,
val name: String,
// Tag type
val type: Int
)
val type: Int
)

View File

@ -2,14 +2,14 @@ package exh.metadata.sql.models
data class SearchTitle(
// Title identifier, unique
val id: Long?,
val id: Long?,
// Metadata this title is attached to
val mangaId: Long,
val mangaId: Long,
// Title
val title: String,
val title: String,
// Title type, useful for distinguishing between main/alt titles
val type: Int
val type: Int
)

View File

@ -3,7 +3,6 @@ package exh.metadata.sql.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
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.tables.SearchMetadataTable
@ -42,4 +41,4 @@ interface SearchMetadataQueries : DbProvider {
.table(SearchMetadataTable.TABLE)
.build())
.prepare()
}
}

View File

@ -44,4 +44,4 @@ interface SearchTagQueries : DbProvider {
}
}
}
}
}

View File

@ -4,8 +4,6 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
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.tables.SearchTitleTable
@ -46,4 +44,4 @@ interface SearchTitleQueries : DbProvider {
}
}
}
}
}

View File

@ -7,7 +7,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
if(!response.isSuccessful) {
if (!response.isSuccessful) {
response.interceptAsHtml { doc ->
// Find captcha
if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) {
@ -20,4 +20,4 @@ val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
}
}
} else response
}
}

View File

@ -1,4 +1,6 @@
package exh.search
class Namespace(var namespace: String,
var tag: Text? = null) : QueryComponent()
class Namespace(
var namespace: String,
var tag: Text? = null
) : QueryComponent()

View File

@ -3,4 +3,4 @@ package exh.search
open class QueryComponent {
var excluded = false
var exact = false
}
}

View File

@ -7,8 +7,10 @@ import exh.metadata.sql.tables.SearchTitleTable
class SearchEngine {
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
fun textToSubQueries(namespace: String?,
component: Text?): Pair<String, List<String>>? {
fun textToSubQueries(
namespace: String?,
component: Text?
): Pair<String, List<String>>? {
val maybeLenientComponent = component?.let {
if (!it.exact)
it.asLenientTagQueries()
@ -22,20 +24,20 @@ class SearchEngine {
"${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?"
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params
}
return if(namespace != null) {
return if (namespace != null) {
var query = """
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
""".trimIndent()
val params = mutableListOf(escapeLike(namespace))
if(componentTagQuery != null) {
if (componentTagQuery != null) {
query += "\n AND ${componentTagQuery.first}"
params += componentTagQuery.second
}
"$query)" to params
} else if(component != null) {
} else if (component != null) {
// Match title + tags
val tagQuery = """
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 exclude = mutableListOf<Pair<String, List<String>>>()
for(component in q) {
val query = if(component is Text) {
for (component in q) {
val query = if (component is Text) {
textToSubQueries(null, component)
} else if(component is Namespace) {
if(component.namespace == "uploader") {
} else if (component is Namespace) {
if (component.namespace == "uploader") {
wheres += "meta.${SearchMetadataTable.COL_UPLOADER} LIKE ?"
whereParams += component.tag!!.rawTextEscapedForLike()
null
} else {
if(component.tag!!.components.size > 0) {
//Match namespace + tags
if (component.tag!!.components.size > 0) {
// Match namespace + tags
textToSubQueries(component.namespace, component.tag)
} else {
//Perform namespace search
// Perform namespace search
textToSubQueries(component.namespace, null)
}
}
} else error("Unknown query component!")
if(query != null) {
(if(component.excluded) exclude else include) += query
if (query != null) {
(if (component.excluded) exclude else include) += query
}
}
@ -97,14 +99,13 @@ class SearchEngine {
completeParams += pair.second
}
exclude.forEach {
wheres += """
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
""".trimIndent()
whereParams += it.second
}
if(wheres.isNotEmpty()) {
if (wheres.isNotEmpty()) {
completeParams += whereParams
baseQuery += "\nWHERE\n"
baseQuery += wheres.joinToString("\nAND\n")
@ -126,7 +127,7 @@ class SearchEngine {
var nextIsExact = false
fun flushText() {
if(queuedRawText.isNotEmpty()) {
if (queuedRawText.isNotEmpty()) {
queuedText += StringTextComponent(queuedRawText.toString())
queuedRawText.setLength(0)
}
@ -150,24 +151,24 @@ class SearchEngine {
}
}
for(char in query.toLowerCase()) {
if(char == '"') {
for (char in query.toLowerCase()) {
if (char == '"') {
inQuotes = !inQuotes
} else if(enableWildcard && (char == '?' || char == '_')) {
} else if (enableWildcard && (char == '?' || char == '_')) {
flushText()
queuedText.add(SingleWildcard(char.toString()))
} else if(enableWildcard && (char == '*' || char == '%')) {
} else if (enableWildcard && (char == '*' || char == '%')) {
flushText()
queuedText.add(MultiWildcard(char.toString()))
} else if(char == '-') {
} else if (char == '-') {
nextIsExcluded = true
} else if(char == '$') {
} else if (char == '$') {
nextIsExact = true
} else if(char == ':') {
} else if (char == ':') {
flushText()
var flushed = flushToText().rawTextOnly()
//Map tag aliases
flushed = when(flushed) {
// Map tag aliases
flushed = when (flushed) {
"a" -> "artist"
"c", "char" -> "character"
"f" -> "female"
@ -179,7 +180,7 @@ class SearchEngine {
else -> flushed
}
namespace = Namespace(flushed, null)
} else if(char == ' ' && !inQuotes) {
} else if (char == ' ' && !inQuotes) {
flushAll()
} else {
queuedRawText.append(char)
@ -197,7 +198,6 @@ class SearchEngine {
return string.replace("\\", "\\\\")
.replace("_", "\\_")
.replace("%", "\\%")
}
}
}

View File

@ -3,7 +3,7 @@ package exh.search
import exh.plusAssign
import exh.search.SearchEngine.Companion.escapeLike
class Text: QueryComponent() {
class Text : QueryComponent() {
val components = mutableListOf<TextComponent>()
private var query: String? = null
@ -12,26 +12,26 @@ class Text: QueryComponent() {
private var rawText: String? = null
fun asQuery(): String {
if(query == null) {
if (query == null) {
query = rBaseBuilder().toString()
}
return query!!
}
fun asLenientTitleQuery(): String {
if(lenientTitleQuery == null) {
if (lenientTitleQuery == null) {
lenientTitleQuery = StringBuilder("%").append(rBaseBuilder()).append("%").toString()
}
return lenientTitleQuery!!
}
fun asLenientTagQueries(): List<String> {
if(lenientTagQueries == null) {
if (lenientTagQueries == null) {
lenientTagQueries = listOf(
//Match beginning of tag
// Match beginning of tag
rBaseBuilder().append("%").toString(),
//Tag word matcher (that matches multiple words)
//Can't make it match a single word in Realm :(
// Tag word matcher (that matches multiple words)
// Can't make it match a single word in Realm :(
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
StringBuilder(" ").append(rBaseBuilder()).toString(),
rBaseBuilder().append(" ").toString()
@ -42,8 +42,8 @@ class Text: QueryComponent() {
fun rBaseBuilder(): StringBuilder {
val builder = StringBuilder()
for(component in components) {
when(component) {
for (component in components) {
when (component) {
is StringTextComponent -> builder += escapeLike(component.value)
is SingleWildcard -> builder += "_"
is MultiWildcard -> builder += "%"
@ -52,7 +52,7 @@ class Text: QueryComponent() {
return builder
}
fun rawTextOnly() = if(rawText != null)
fun rawTextOnly() = if (rawText != null)
rawText!!
else {
rawText = components

View File

@ -8,13 +8,19 @@ import eu.kanade.tachiyomi.source.model.SManga
import exh.ui.smartsearch.SmartSearchPresenter
import exh.util.await
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 uy.kohesive.injekt.injectLazy
import kotlin.coroutines.CoroutineContext
class SmartSearchEngine(parentContext: CoroutineContext,
val extraSearchParams: String? = null): CoroutineScope {
class SmartSearchEngine(
parentContext: CoroutineContext,
val extraSearchParams: String? = null
) : CoroutineScope {
override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default
private val db: DatabaseHelper by injectLazy()
@ -29,7 +35,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
val eligibleManga = supervisorScope {
queries.map { query ->
async(Dispatchers.Default) {
val builtQuery = if(extraSearchParams != null) {
val builtQuery = if (extraSearchParams != null) {
"$query ${extraSearchParams.trim()}"
} else query
@ -51,7 +57,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
val eligibleManga = supervisorScope {
val searchQuery = if(extraSearchParams != null) {
val searchQuery = if (extraSearchParams != null) {
"$title ${extraSearchParams.trim()}"
} else title
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
@ -71,7 +77,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
val splitCleanedTitle = cleanedTitle.split(" ")
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
if(splitCleanedTitle.isEmpty()) {
if (splitCleanedTitle.isEmpty()) {
return emptyList()
}
@ -99,7 +105,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
// Remove text in brackets
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)
}
@ -127,7 +133,7 @@ class SmartSearchEngine(parentContext: CoroutineContext,
}.toMap()
// Reverse pairs if reading backwards
if(!readForward) {
if (!readForward) {
val tmp = openingBracketPairs
openingBracketPairs = closingBracketPairs
closingBracketPairs = tmp
@ -136,16 +142,16 @@ class SmartSearchEngine(parentContext: CoroutineContext,
val depthPairs = bracketPairs.map { 0 }.toMutableList()
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]
if(openingBracketDepthIndex != null) {
if (openingBracketDepthIndex != null) {
depthPairs[openingBracketDepthIndex]++
} else {
val closingBracketDepthIndex = closingBracketPairs[c]
if(closingBracketDepthIndex != null) {
if (closingBracketDepthIndex != null) {
depthPairs[closingBracketDepthIndex]--
} else {
if(depthPairs.all { it <= 0 }) {
if (depthPairs.all { it <= 0 }) {
result.append(c)
} else {
// 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 consecutiveSpacesRegex = Regex(" +")
}
}
}

View File

@ -1,29 +1,32 @@
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 okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
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.
*
* @param page the page number to retrieve.
*/
override fun popularMangaRequest(page: Int)
= throw UnsupportedOperationException("Should never be called!")
override fun popularMangaRequest(page: Int) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun popularMangaParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun popularMangaParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* 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 filters the list of filters to apply.
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
= throw UnsupportedOperationException("Should never be called!")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun searchMangaParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun searchMangaParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
override fun latestUpdatesRequest(page: Int)
= throw UnsupportedOperationException("Should never be called!")
override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun latestUpdatesParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
*/
override fun mangaDetailsParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun mangaDetailsParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
override fun chapterListParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun chapterListParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun pageListParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* 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()
private fun ensureDelegateCompatible() {
if(versionId != delegate.versionId
|| lang != delegate.lang) {
if (versionId != delegate.versionId ||
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 {
delegate.bindDelegate(this)
}
}
}

View File

@ -2,13 +2,19 @@ package exh.source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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 okhttp3.Response
import uy.kohesive.injekt.injectLazy
class EnhancedHttpSource(val originalSource: HttpSource,
val enchancedSource: HttpSource): HttpSource() {
class EnhancedHttpSource(
val originalSource: HttpSource,
val enchancedSource: HttpSource
) : HttpSource() {
private val prefs: PreferencesHelper by injectLazy()
/**
@ -16,16 +22,16 @@ class EnhancedHttpSource(val originalSource: HttpSource,
*
* @param page the page number to retrieve.
*/
override fun popularMangaRequest(page: Int)
= throw UnsupportedOperationException("Should never be called!")
override fun popularMangaRequest(page: Int) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun popularMangaParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun popularMangaParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* 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 filters the list of filters to apply.
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
= throw UnsupportedOperationException("Should never be called!")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun searchMangaParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun searchMangaParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
override fun latestUpdatesRequest(page: Int)
= throw UnsupportedOperationException("Should never be called!")
override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun latestUpdatesParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
*/
override fun mangaDetailsParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun mangaDetailsParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
override fun chapterListParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun chapterListParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun pageListParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* Parses the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response)
= throw UnsupportedOperationException("Should never be called!")
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("Should never be called!")
/**
* 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 filters the list of filters to apply.
*/
override fun fetchSearchManga(page: Int, query: String, filters: FilterList)
= source().fetchSearchManga(page, query, filters)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
source().fetchSearchManga(page, query, filters)
/**
* 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 manga the manga of the chapter.
*/
override fun prepareNewChapter(chapter: SChapter, manga: SManga)
= source().prepareNewChapter(chapter, manga)
override fun prepareNewChapter(chapter: SChapter, manga: SManga) =
source().prepareNewChapter(chapter, manga)
/**
* Returns the list of filters for the source.
@ -211,10 +217,10 @@ class EnhancedHttpSource(val originalSource: HttpSource,
override fun getFilterList() = source().getFilterList()
private fun source(): HttpSource {
return if(prefs.eh_delegateSources().getOrDefault()) {
return if (prefs.eh_delegateSources().getOrDefault()) {
enchancedSource
} else {
originalSource
}
}
}
}

View File

@ -7,14 +7,14 @@ import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast
import timber.log.Timber
import kotlin.concurrent.thread
import timber.log.Timber
class ConfiguringDialogController : DialogController() {
private var materialDialog: MaterialDialog? = null
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
if(savedViewState == null)
if (savedViewState == null)
thread {
try {
EHConfigurator().configureAll()
@ -62,4 +62,3 @@ class ConfiguringDialogController : DialogController() {
router.popController(this)
}
}

View File

@ -24,11 +24,13 @@ class EHConfigurator {
private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder()
.addHeader("Cookie", cookiesHeader(sp))
private fun EHentai.execProfileActions(action: String,
name: String,
set: String,
sp: Int)
= configuratorClient.newCall(requestWithCreds(sp)
private fun EHentai.execProfileActions(
action: String,
name: String,
set: String,
sp: Int
) =
configuratorClient.newCall(requestWithCreds(sp)
.url(uconfigUrl)
.post(FormBody.Builder()
.add("profile_action", action)
@ -44,7 +46,7 @@ class EHConfigurator {
val ehSource = sources.get(EH_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()
.url(HATH_PERKS_URL)
.build())
@ -56,13 +58,13 @@ class EHConfigurator {
val name = it.child(0).text().toLowerCase()
val purchased = it.child(2).getElementsByTag("form").isEmpty()
when(name) {
//Thumbnail rows
when (name) {
// Thumbnail rows
"more thumbs" -> hathPerks.moreThumbs = purchased
"thumbs up" -> hathPerks.thumbsUp = purchased
"all thumbs" -> hathPerks.allThumbs = purchased
//Pagination sizing
// Pagination sizing
"paging enlargement i" -> hathPerks.pagingEnlargementI = purchased
"paging enlargement ii" -> hathPerks.pagingEnlargementII = purchased
"paging enlargement iii" -> hathPerks.pagingEnlargementIII = purchased
@ -76,45 +78,45 @@ class EHConfigurator {
}
fun configure(source: EHentai, hathPerks: EHHathPerksResponse) {
//Delete old app profiles
// Delete old app profiles
val scanReq = source.requestWithCreds().url(source.uconfigUrl).build()
val resp = configuratorClient.newCall(scanReq).execute().asJsoup()
var lastDoc = resp
resp.select(PROFILE_SELECTOR).forEach {
if(it.text() == PROFILE_NAME) {
if (it.text() == PROFILE_NAME) {
val id = it.attr("value")
//Delete old profile
// Delete old profile
lastDoc = source.execProfileActions("delete", "", id, id.toInt()).asJsoup()
}
}
//Find available profile slot
val availableProfiles = (1 .. 3).toMutableList()
// Find available profile slot
val availableProfiles = (1..3).toMutableList()
lastDoc.select(PROFILE_SELECTOR).forEach {
availableProfiles.remove(it.attr("value").toInt())
}
//No profile slots left :(
if(availableProfiles.isEmpty())
// No profile slots left :(
if (availableProfiles.isEmpty())
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 response = source.execProfileActions("create",
PROFILE_NAME,
slot.toString(),
1)
//Build new profile
// Build new profile
val form = EhUConfigBuilder().build(hathPerks)
//Send new profile to server
// Send new profile to server
configuratorClient.newCall(source.requestWithCreds(sp = slot)
.url(source.uconfigUrl)
.post(form)
.build()).execute()
//Persist slot + sk
// Persist slot + sk
source.spPref().set(slot)
val keyCookie = response.headers.toMultimap()["Set-Cookie"]?.find {
@ -127,19 +129,19 @@ class EHConfigurator {
it.startsWith("hath_perks=")
}?.removePrefix("hath_perks=")?.substringBefore(';')
if(keyCookie != null)
if (keyCookie != null)
prefs.eh_settingsKey().set(keyCookie)
if(sessionCookie != null)
if (sessionCookie != null)
prefs.eh_sessionCookie().set(sessionCookie)
if(hathPerksCookie != null)
if (hathPerksCookie != null)
prefs.eh_hathPerksCookies().set(hathPerksCookie)
}
companion object {
private const val PROFILE_NAME = "TachiyomiEH App"
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 PROFILE_SELECTOR = "[name=profile_set] > option"
}
}
}

View File

@ -9,7 +9,6 @@ class EHHathPerksResponse {
var pagingEnlargementII = false
var pagingEnlargementIII = false
override fun toString()
= "EHHathPerksResponse(moreThumbs=$moreThumbs, thumbsUp=$thumbsUp, allThumbs=$allThumbs, pagingEnlargementI=$pagingEnlargementI, pagingEnlargementII=$pagingEnlargementII, pagingEnlargementIII=$pagingEnlargementIII)"
override fun toString() =
"EHHathPerksResponse(moreThumbs=$moreThumbs, thumbsUp=$thumbsUp, allThumbs=$allThumbs, pagingEnlargementI=$pagingEnlargementI, pagingEnlargementII=$pagingEnlargementII, pagingEnlargementIII=$pagingEnlargementIII)"
}

View File

@ -11,7 +11,7 @@ class EhUConfigBuilder {
fun build(hathPerks: EHHathPerksResponse): FormBody {
val configItems = mutableListOf<ConfigItem>()
configItems += when(prefs.imageQuality()
configItems += when (prefs.imageQuality()
.getOrDefault()
.toLowerCase()) {
"ovrs_2400" -> Entry.ImageSize.`2400`
@ -23,17 +23,17 @@ class EhUConfigBuilder {
else -> Entry.ImageSize.AUTO
}
configItems += if(prefs.useHentaiAtHome().getOrDefault())
configItems += if (prefs.useHentaiAtHome().getOrDefault())
Entry.UseHentaiAtHome.YES
else
Entry.UseHentaiAtHome.NO
configItems += if(prefs.useJapaneseTitle().getOrDefault())
configItems += if (prefs.useJapaneseTitle().getOrDefault())
Entry.TitleDisplayLanguage.JAPANESE
else
Entry.TitleDisplayLanguage.DEFAULT
configItems += if(prefs.eh_useOriginalImages().getOrDefault())
configItems += if (prefs.eh_useOriginalImages().getOrDefault())
Entry.UseOriginalImages.YES
else
Entry.UseOriginalImages.NO
@ -56,7 +56,7 @@ class EhUConfigBuilder {
configItems += Entry.UseMPV()
configItems += Entry.ShowPopularRightNowPane()
//Actually build form body
// Actually build form body
val formBody = FormBody.Builder()
configItems.forEach {
formBody.add(it.key, it.value)
@ -67,14 +67,14 @@ class EhUConfigBuilder {
}
object Entry {
enum class UseHentaiAtHome(override val value: String): ConfigItem {
enum class UseHentaiAtHome(override val value: String) : ConfigItem {
YES("0"),
NO("1");
override val key = "uh"
}
enum class ImageSize(override val value: String): ConfigItem {
enum class ImageSize(override val value: String) : ConfigItem {
AUTO("0"),
`2400`("5"),
`1600`("4"),
@ -85,20 +85,20 @@ object Entry {
override val key = "xr"
}
enum class TitleDisplayLanguage(override val value: String): ConfigItem {
enum class TitleDisplayLanguage(override val value: String) : ConfigItem {
DEFAULT("0"),
JAPANESE("1");
override val key = "tl"
}
//Locked to extended mode as that's what the parser and toplists use
class DisplayMode: ConfigItem {
// Locked to extended mode as that's what the parser and toplists use
class DisplayMode : ConfigItem {
override val key = "dm"
override val value = "2"
}
enum class SearchResultsCount(override val value: String): ConfigItem {
enum class SearchResultsCount(override val value: String) : ConfigItem {
`25`("0"),
`50`("1"),
`100`("2"),
@ -107,7 +107,7 @@ object Entry {
override val key = "rc"
}
enum class ThumbnailRows(override val value: String): ConfigItem {
enum class ThumbnailRows(override val value: String) : ConfigItem {
`4`("0"),
`10`("1"),
`20`("2"),
@ -116,21 +116,21 @@ object Entry {
override val key = "tr"
}
enum class UseOriginalImages(override val value: String): ConfigItem {
enum class UseOriginalImages(override val value: String) : ConfigItem {
NO("0"),
YES("1");
override val key = "oi"
}
//Locked to no MPV as that's what the parser uses
class UseMPV: ConfigItem {
// Locked to no MPV as that's what the parser uses
class UseMPV : ConfigItem {
override val key = "qb"
override val value = "0"
}
//Locked to no popular pane as we can't parse it
class ShowPopularRightNowPane: ConfigItem {
// Locked to no popular pane as we can't parse it
class ShowPopularRightNowPane : ConfigItem {
override val key = "pp"
override val value = "1"
}
@ -139,4 +139,4 @@ object Entry {
interface ConfigItem {
val key: String
val value: String
}
}

View File

@ -32,10 +32,10 @@ class WarnConfigureDialogController : DialogController() {
companion object {
fun uploadSettings(router: Router) {
if(Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().getOrDefault())
if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().getOrDefault())
WarnConfigureDialogController().showDialog(router)
else
ConfiguringDialogController().showDialog(router)
}
}
}
}

View File

@ -1,18 +1,18 @@
package exh.ui
import java.util.UUID
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
typealias LoadingHandle = String
/**
* Class used to manage loader UIs
*/
class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): CoroutineScope {
class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope {
override val coroutineContext = Dispatchers.Main + parentContext
private val openLoadingHandles = mutableListOf<LoadingHandle>()
@ -25,7 +25,7 @@ class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): Co
handle to (openLoadingHandles.size == 1)
}
if(shouldUpdateLoadingStatus) {
if (shouldUpdateLoadingStatus) {
launch {
updateLoadingStatus(true)
}
@ -36,13 +36,13 @@ class LoaderManager(parentContext: CoroutineContext = EmptyCoroutineContext): Co
@Synchronized
fun closeProgressBar(handle: LoadingHandle?) {
if(handle == null) return
if (handle == null) return
val shouldUpdateLoadingStatus = synchronized(this) {
openLoadingHandles.remove(handle) && openLoadingHandles.isEmpty()
}
if(shouldUpdateLoadingStatus) {
if (shouldUpdateLoadingStatus) {
launch {
updateLoadingStatus(false)
}

View File

@ -6,11 +6,11 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import eu.kanade.tachiyomi.ui.base.controller.BaseController
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext
abstract class BaseExhController(bundle: Bundle? = null) : BaseController(bundle), CoroutineScope {
abstract val layoutId: Int

View File

@ -1,6 +1,5 @@
package exh.ui.batchadd
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -44,14 +43,14 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy {
progressSubscriptions.clear()
if(it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
showProgress(this)
progressSubscriptions += presenter.progressRelay
.observeOn(AndroidSchedulers.mainThread())
.combineLatest(presenter.progressTotalRelay, { progress, total ->
//Show hide dismiss button
// Show hide dismiss button
progress_dismiss_btn.visibility =
if(progress == total)
if (progress == total)
View.VISIBLE
else View.GONE
@ -79,7 +78,7 @@ class BatchAddController : NucleusController<BatchAddPresenter>() {
}?.let {
progressSubscriptions += it
}
} else if(it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
} else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
hideProgress(this)
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 addGalleries(galleries: String) {
//Check text box has content
if(galleries.isBlank()) {
// Check text box has content
if (galleries.isBlank()) {
noGalleriesSpecified()
return
}

View File

@ -8,7 +8,7 @@ import exh.GalleryAdder
import exh.metadata.nullIfBlank
import kotlin.concurrent.thread
class BatchAddPresenter: BasePresenter<BatchAddController>() {
class BatchAddPresenter : BasePresenter<BatchAddController>() {
private val galleryAdder by lazy { GalleryAdder() }
@ -34,7 +34,7 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
splitGalleries.forEachIndexed { i, s ->
val result = galleryAdder.addGallery(s, true)
if(result is GalleryAddEvent.Success) {
if (result is GalleryAddEvent.Success) {
succeeded.add(s)
} else {
failed.add(s)
@ -46,7 +46,7 @@ class BatchAddPresenter: BasePresenter<BatchAddController>() {
}) + " " + result.logMessage)
}
//Show report
// Show report
val summary = "\nSummary:\nAdded: ${succeeded.size} gallerie(s)\nFailed: ${failed.size} gallerie(s)"
eventRelay?.call(summary)
}

View File

@ -7,21 +7,23 @@ import android.webkit.WebView
import androidx.annotation.RequiresApi
import eu.kanade.tachiyomi.util.system.asJsoup
import exh.ui.captcha.BrowserActionActivity.Companion.CROSS_WINDOW_SCRIPT_INNER
import java.nio.charset.Charset
import org.jsoup.nodes.DataNode
import org.jsoup.nodes.Element
import java.nio.charset.Charset
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class AutoSolvingWebViewClient(activity: BrowserActionActivity,
verifyComplete: (String) -> Boolean,
injectScript: String?,
headers: Map<String, String>)
: HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) {
class AutoSolvingWebViewClient(
activity: BrowserActionActivity,
verifyComplete: (String) -> Boolean,
injectScript: String?,
headers: Map<String, String>
) :
HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
// Inject our custom script into the recaptcha iframes
val lastPathSegment = request.url.pathSegments.lastOrNull()
if(lastPathSegment == "anchor" || lastPathSegment == "bframe") {
if (lastPathSegment == "anchor" || lastPathSegment == "bframe") {
val oReq = request.toOkHttpRequest()
val response = activity.httpClient.newCall(oReq).execute()
val doc = response.asJsoup()
@ -34,4 +36,4 @@ class AutoSolvingWebViewClient(activity: BrowserActionActivity,
}
return super.shouldInterceptRequest(view, request)
}
}
}

View File

@ -4,17 +4,19 @@ import android.os.Build
import android.webkit.WebView
import android.webkit.WebViewClient
open class BasicWebViewClient(protected val activity: BrowserActionActivity,
protected val verifyComplete: (String) -> Boolean,
private val injectScript: String?) : WebViewClient() {
open class BasicWebViewClient(
protected val activity: BrowserActionActivity,
protected val verifyComplete: (String) -> Boolean,
private val injectScript: String?
) : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
if(verifyComplete(url)) {
if (verifyComplete(url)) {
activity.finish()
} 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 File

@ -6,7 +6,12 @@ import android.os.Build
import android.os.Bundle
import android.os.SystemClock
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.appcompat.app.AppCompatActivity
import com.afollestad.materialdialogs.MaterialDialog
@ -22,6 +27,9 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import exh.source.DelegatedHttpSource
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 okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -33,10 +41,6 @@ import rx.Single
import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.Serializable
import java.net.URL
import java.util.*
import kotlin.collections.HashMap
class BrowserActionActivity : AppCompatActivity() {
private val sourceManager: SourceManager by injectLazy()
@ -58,8 +62,8 @@ class BrowserActionActivity : AppCompatActivity() {
setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha)
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
val originalSource = if(sourceId != -1L) sourceManager.get(sourceId) else null
val source = if(originalSource != null) {
val originalSource = if (sourceId != -1L) sourceManager.get(sourceId) else null
val source = if (originalSource != null) {
originalSource as? ActionCompletionVerifier
?: run {
(originalSource as? HttpSource)?.let {
@ -72,24 +76,24 @@ class BrowserActionActivity : AppCompatActivity() {
it.value.joinToString(",")
} ?: emptyMap()) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
val cookies: HashMap<String, String>?
= intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
val cookies: HashMap<String, String>? =
intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
val url: String? = intent.getStringExtra(URL_EXTRA)
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
val verifyComplete = if(source != null) {
val verifyComplete = if (source != null) {
source::verifyComplete!!
} else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean
if(verifyComplete == null || url == null) {
if (verifyComplete == null || url == null) {
finish()
return
}
val actionStr = actionName ?: "Solve captcha"
toolbar.title = if(source != null) {
toolbar.title = if (source != null) {
"${source.name}: $actionStr"
} else actionStr
@ -115,13 +119,13 @@ class BrowserActionActivity : AppCompatActivity() {
webview.webChromeClient = object : WebChromeClient() {
override fun onJsAlert(view: WebView?, url: String?, message: String, result: JsResult): Boolean {
if(message.startsWith("exh-")) {
if (message.startsWith("exh-")) {
loadedInners++
// Wait for both inner scripts to be loaded
if(loadedInners >= 2) {
if (loadedInners >= 2) {
// Attempt to autosolve captcha
if(preferencesHelper.eh_autoSolveCaptchas().getOrDefault()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (preferencesHelper.eh_autoSolveCaptchas().getOrDefault() &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webview.post {
// 10 seconds to auto-solve captcha
strictValidationStartTime = System.currentTimeMillis() + 1000 * 10
@ -141,7 +145,7 @@ class BrowserActionActivity : AppCompatActivity() {
}
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
credentialsObservable = httpClient.newCall(Request.Builder()
// Rob demo credentials
@ -196,11 +200,11 @@ class BrowserActionActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@JavascriptInterface
fun callback(result: String?, loopId: String, stage: Int) {
if(loopId != currentLoopId) return
if (loopId != currentLoopId) return
when(stage) {
when (stage) {
STAGE_CHECKBOX -> {
if(result!!.toBoolean()) {
if (result!!.toBoolean()) {
webview.postDelayed({
getAudioButtonLocation(loopId)
}, 250)
@ -211,7 +215,7 @@ class BrowserActionActivity : AppCompatActivity() {
}
}
STAGE_GET_AUDIO_BTN_LOCATION -> {
if(result != null) {
if (result != null) {
val splitResult = result.split(" ").map { it.toFloat() }
val origX = splitResult[0]
val origY = splitResult[1]
@ -231,11 +235,11 @@ class BrowserActionActivity : AppCompatActivity() {
}
}
STAGE_DOWNLOAD_AUDIO -> {
if(result != null) {
if (result != null) {
Timber.d("Got audio URL: $result")
performRecognize(result)
.observeOn(Schedulers.io())
.subscribe ({
.subscribe({
Timber.d("Got audio transcript: $it")
webview.post {
typeResult(loopId, it!!
@ -253,7 +257,7 @@ class BrowserActionActivity : AppCompatActivity() {
}
}
STAGE_TYPE_RESULT -> {
if(result!!.toBoolean()) {
if (result!!.toBoolean()) {
// Fail if captcha still not solved after 1.5s
strictValidationStartTime = System.currentTimeMillis() + 1500
} else {
@ -293,7 +297,7 @@ class BrowserActionActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun doStageCheckbox(loopId: String) {
if(loopId != currentLoopId) return
if (loopId != currentLoopId) return
webview.evaluateJavascript("""
(function() {
@ -415,27 +419,26 @@ class BrowserActionActivity : AppCompatActivity() {
doStageCheckbox(loopId)
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@JavascriptInterface
fun validateCaptchaCallback(result: Boolean, loopId: String) {
if(loopId != validateCurrentLoopId) return
if (loopId != validateCurrentLoopId) return
if(result) {
if (result) {
Timber.d("Captcha solved!")
webview.post {
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
}
val asbtn = intent.getStringExtra(ASBTN_EXTRA)
if(asbtn != null) {
if (asbtn != null) {
webview.post {
webview.evaluateJavascript("(function() {document.querySelector('$asbtn').click();})();", null)
}
}
} else {
val savedStrictValidationStartTime = strictValidationStartTime
if(savedStrictValidationStartTime != null
&& System.currentTimeMillis() > savedStrictValidationStartTime) {
if (savedStrictValidationStartTime != null &&
System.currentTimeMillis() > savedStrictValidationStartTime) {
captchaSolveFail()
} else {
webview.postDelayed({
@ -447,7 +450,7 @@ class BrowserActionActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun runValidateCaptcha(loopId: String) {
if(loopId != validateCurrentLoopId) return
if (loopId != validateCurrentLoopId) return
webview.evaluateJavascript("""
(function() {
@ -624,12 +627,14 @@ class BrowserActionActivity : AppCompatActivity() {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
fun launchCaptcha(context: Context,
source: ActionCompletionVerifier,
cookies: Map<String, String>,
script: String?,
url: String,
autoSolveSubmitBtnSelector: String? = null) {
fun launchCaptcha(
context: Context,
source: ActionCompletionVerifier,
cookies: Map<String, String>,
script: String?,
url: String,
autoSolveSubmitBtnSelector: String? = null
) {
val intent = baseIntent(context).apply {
putExtra(SOURCE_ID_EXTRA, source.id)
putExtra(COOKIES_EXTRA, HashMap(cookies))
@ -641,9 +646,11 @@ class BrowserActionActivity : AppCompatActivity() {
context.startActivity(intent)
}
fun launchUniversal(context: Context,
source: HttpSource,
url: String) {
fun launchUniversal(
context: Context,
source: HttpSource,
url: String
) {
val intent = baseIntent(context).apply {
putExtra(SOURCE_ID_EXTRA, source.id)
putExtra(URL_EXTRA, url)
@ -652,9 +659,11 @@ class BrowserActionActivity : AppCompatActivity() {
context.startActivity(intent)
}
fun launchUniversal(context: Context,
sourceId: Long,
url: String) {
fun launchUniversal(
context: Context,
sourceId: Long,
url: String
) {
val intent = baseIntent(context).apply {
putExtra(SOURCE_ID_EXTRA, sourceId)
putExtra(URL_EXTRA, url)
@ -663,11 +672,13 @@ class BrowserActionActivity : AppCompatActivity() {
context.startActivity(intent)
}
fun launchAction(context: Context,
completionVerifier: ActionCompletionVerifier,
script: String?,
url: String,
actionName: String) {
fun launchAction(
context: Context,
completionVerifier: ActionCompletionVerifier,
script: String?,
url: String,
actionName: String
) {
val intent = baseIntent(context).apply {
putExtra(SOURCE_ID_EXTRA, completionVerifier.id)
putExtra(SCRIPT_EXTRA, script)
@ -678,12 +689,14 @@ class BrowserActionActivity : AppCompatActivity() {
context.startActivity(intent)
}
fun launchAction(context: Context,
completionVerifier: (String) -> Boolean,
script: String?,
url: String,
actionName: String,
headers: Map<String, String>? = emptyMap()) {
fun launchAction(
context: Context,
completionVerifier: (String) -> Boolean,
script: String?,
url: String,
actionName: String,
headers: Map<String, String>? = emptyMap()
) {
val intent = baseIntent(context).apply {
putExtra(HEADERS_EXTRA, HashMap(headers))
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 {
override val versionId get() = source.versionId
override val lang: String get() = source.lang
@ -708,4 +721,3 @@ class NoopActionCompletionVerifier(private val source: HttpSource): DelegatedHtt
interface ActionCompletionVerifier : Source {
fun verifyComplete(url: String): Boolean
}

View File

@ -7,11 +7,13 @@ import android.webkit.WebView
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
open class HeadersInjectingWebViewClient(activity: BrowserActionActivity,
verifyComplete: (String) -> Boolean,
injectScript: String?,
private val headers: Map<String, String>)
: BasicWebViewClient(activity, verifyComplete, injectScript) {
open class HeadersInjectingWebViewClient(
activity: BrowserActionActivity,
verifyComplete: (String) -> Boolean,
injectScript: String?,
private val headers: Map<String, String>
) :
BasicWebViewClient(activity, verifyComplete, injectScript) {
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
// Temp disabled as it's unreliable

View File

@ -16,4 +16,4 @@ fun WebResourceRequest.toOkHttpRequest(): Request {
}
return request.build()
}
}

View File

@ -23,7 +23,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
super.onCreate(savedInstanceState)
setContentView(R.layout.eh_activity_intercept)
//Show back button
// Show back button
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@ -31,7 +31,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
}
private fun processLink() {
if(Intent.ACTION_VIEW == intent.action) {
if (Intent.ACTION_VIEW == intent.action) {
intercept_progress.visible()
intercept_status.text = "Loading gallery..."
presenter.loadGallery(intent.dataString)
@ -52,7 +52,7 @@ class InterceptActivity : BaseRxActivity<InterceptActivityPresenter>() {
statusSubscription = presenter.status
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
when(it) {
when (it) {
is InterceptResult.Success -> {
intercept_progress.gone()
intercept_status.text = "Launching app..."

View File

@ -3,8 +3,8 @@ package exh.ui.intercept
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import exh.GalleryAddEvent
import exh.GalleryAdder
import rx.subjects.BehaviorSubject
import kotlin.concurrent.thread
import rx.subjects.BehaviorSubject
class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
private val galleryAdder = GalleryAdder()
@ -13,11 +13,11 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
@Synchronized
fun loadGallery(gallery: String) {
//Do not load gallery if already loading
if(status.value is InterceptResult.Idle) {
// Do not load gallery if already loading
if (status.value is InterceptResult.Idle) {
status.onNext(InterceptResult.Loading())
//Load gallery async
// Load gallery async
thread {
val result = galleryAdder.addGallery(gallery)
@ -35,6 +35,6 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
sealed class InterceptResult {
class Idle : InterceptResult()
class Loading : InterceptResult()
data class Success(val mangaId: Long): InterceptResult()
data class Failure(val reason: String): InterceptResult()
}
data class Success(val mangaId: Long) : InterceptResult()
data class Failure(val reason: String) : InterceptResult()
}

View File

@ -28,21 +28,21 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
val prefs: PreferencesHelper by injectLazy()
val fingerprintSupported
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Reprint.isHardwarePresent()
&& Reprint.hasFingerprintRegistered()
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
Reprint.isHardwarePresent() &&
Reprint.hasFingerprintRegistered()
val useFingerprint
get() = fingerprintSupported
&& prefs.eh_lockUseFingerprint().getOrDefault()
get() = fingerprintSupported &&
prefs.eh_lockUseFingerprint().getOrDefault()
@SuppressLint("NewApi")
override fun onAttached() {
super.onAttached()
if(fingerprintSupported) {
if (fingerprintSupported) {
updateSummary()
onChange {
if(it as Boolean)
if (it as Boolean)
tryChange()
else
prefs.eh_lockUseFingerprint().set(false)
@ -51,7 +51,7 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
} else {
title = "Fingerprint unsupported"
shouldDisableView = true
summary = if(!Reprint.hasFingerprintRegistered())
summary = if (!Reprint.hasFingerprintRegistered())
"No fingerprints enrolled!"
else
"Fingerprint unlock is unsupported on this device!"
@ -61,7 +61,7 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
private fun updateSummary() {
isChecked = useFingerprint
title = if(isChecked)
title = if (isChecked)
"Fingerprint enabled"
else
"Fingerprint disabled"
@ -146,4 +146,4 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
subscription.unsubscribe()
}
}
}
}

View File

@ -1,6 +1,5 @@
package exh.ui.lock
import android.content.Intent
import android.view.WindowManager
import androidx.fragment.app.FragmentActivity
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.getOrDefault
import uy.kohesive.injekt.injectLazy
import java.util.Date
object LockActivityDelegate {
private val preferences by injectLazy<PreferencesHelper>()
@ -20,7 +18,6 @@ object LockActivityDelegate {
.popChangeHandler(LockChangeHandler(animate)))
}
fun onCreate(activity: FragmentActivity) {
preferences.secureScreen().asObservable()
.subscribe {
@ -42,5 +39,4 @@ object LockActivityDelegate {
private fun isAppLocked(router: Router): Boolean {
return router.backstack.lastOrNull()?.controller() is LockController
}
}

View File

@ -7,10 +7,10 @@ import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
import java.util.*
import java.util.ArrayList
class LockChangeHandler : AnimatorChangeHandler {
constructor(): super()
constructor() : super()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
@ -36,6 +36,4 @@ class LockChangeHandler : AnimatorChangeHandler {
override fun copy(): ControllerChangeHandler =
LockChangeHandler(animationDuration, removesFromViewOnPush())
}

Some files were not shown because too many files have changed in this diff Show More