Linting Fixes AZ

This commit is contained in:
Jobobby04 2020-05-02 00:46:24 -04:00
parent 03e5c5ca10
commit 7e99a9f789
108 changed files with 2962 additions and 2412 deletions

View File

@ -83,10 +83,12 @@ class ChapterCache(private val context: Context) {
// --> EH // --> EH
// Cache size is in MB // Cache size is in MB
private fun setupDiskCache(cacheSize: Long): DiskLruCache { private fun setupDiskCache(cacheSize: Long): DiskLruCache {
return DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), return DiskLruCache.open(
PARAMETER_APP_VERSION, File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_VALUE_COUNT, PARAMETER_APP_VERSION,
cacheSize * 1024 * 1024) PARAMETER_VALUE_COUNT,
cacheSize * 1024 * 1024
)
} }
// <-- EH // <-- EH

View File

@ -19,18 +19,22 @@ interface ChapterQueries : DbProvider {
fun getChaptersByMangaId(mangaId: Long?) = db.get() fun getChaptersByMangaId(mangaId: Long?) = db.get()
.listOfObjects(Chapter::class.java) .listOfObjects(Chapter::class.java)
.withQuery(Query.builder() .withQuery(
Query.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?") .where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId) .whereArgs(mangaId)
.build()) .build()
)
.prepare() .prepare()
fun getChaptersByMergedMangaId(mangaId: Long) = db.get() fun getChaptersByMergedMangaId(mangaId: Long) = db.get()
.listOfObjects(Chapter::class.java) .listOfObjects(Chapter::class.java)
.withQuery(RawQuery.builder() .withQuery(
RawQuery.builder()
.query(getMergedChaptersQuery(mangaId)) .query(getMergedChaptersQuery(mangaId))
.build()) .build()
)
.prepare() .prepare()
fun getRecentChapters(date: Date) = db.get() fun getRecentChapters(date: Date) = db.get()
@ -80,11 +84,13 @@ interface ChapterQueries : DbProvider {
fun getChapters(url: String) = db.get() fun getChapters(url: String) = db.get()
.listOfObjects(Chapter::class.java) .listOfObjects(Chapter::class.java)
.withQuery(Query.builder() .withQuery(
Query.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?") .where("${ChapterTable.COL_URL} = ?")
.whereArgs(url) .whereArgs(url)
.build()) .build()
)
.prepare() .prepare()

View File

@ -10,7 +10,8 @@ import eu.kanade.tachiyomi.data.database.tables.MergedTable as Merged
/** /**
* Query to get the manga merged into a merged manga * Query to get the manga merged into a merged manga
*/ */
fun getMergedMangaQuery(id: Long) = """ fun getMergedMangaQuery(id: Long) =
"""
SELECT ${Manga.TABLE}.* SELECT ${Manga.TABLE}.*
FROM ( FROM (
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id
@ -22,7 +23,8 @@ fun getMergedMangaQuery(id: Long) = """
/** /**
* Query to get the chapters of all manga in a merged manga * Query to get the chapters of all manga in a merged manga
*/ */
fun getMergedChaptersQuery(id: Long) = """ fun getMergedChaptersQuery(id: Long) =
"""
SELECT ${Chapter.TABLE}.* SELECT ${Chapter.TABLE}.*
FROM ( FROM (
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id

View File

@ -21,10 +21,10 @@ class MangaUrlPutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_URL, manga.url) put(MangaTable.COL_URL, manga.url)

View File

@ -9,7 +9,8 @@ object MergedTable {
const val COL_MANGA_ID = "mangaID" const val COL_MANGA_ID = "mangaID"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_MERGE_ID INTEGER NOT NULL, $COL_MERGE_ID INTEGER NOT NULL,
$COL_MANGA_ID INTEGER NOT NULL $COL_MANGA_ID INTEGER NOT NULL
)""" )"""

View File

@ -147,7 +147,7 @@ class ExtensionManager(
fun Extension.isBlacklisted( fun Extension.isBlacklisted(
blacklistEnabled: Boolean = blacklistEnabled: Boolean =
preferences.eh_enableSourceBlacklist().get() preferences.eh_enableSourceBlacklist().get()
): Boolean { ): Boolean {
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
} }

View File

@ -37,7 +37,7 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
private fun newMetaInstance() = metaClass.constructors.find { private fun newMetaInstance() = metaClass.constructors.find {
it.parameters.isEmpty() it.parameters.isEmpty()
}?.call() }?.call()
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!") ?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
/** /**
* Parses metadata from the input and then copies it into the manga * Parses metadata from the input and then copies it into the manga

View File

@ -19,10 +19,12 @@ interface UrlImportableSource : Source {
return try { return try {
val uri = URI(url) val uri = URI(url)
var out = uri.path var out = uri.path
if (uri.query != null) if (uri.query != null) {
out += "?" + uri.query out += "?" + uri.query
if (uri.fragment != null) }
if (uri.fragment != null) {
out += "#" + uri.fragment out += "#" + uri.fragment
}
out out
} catch (e: URISyntaxException) { } catch (e: URISyntaxException) {
url url

View File

@ -73,16 +73,18 @@ class EHentai(
override val metaClass = EHentaiSearchMetadata::class override val metaClass = EHentaiSearchMetadata::class
val schema: String val schema: String
get() = if (prefs.secureEXH().getOrDefault()) get() = if (prefs.secureEXH().getOrDefault()) {
"https" "https"
else } else {
"http" "http"
}
val domain: String val domain: String
get() = if (exh) get() = if (exh) {
"exhentai.org" "exhentai.org"
else } else {
"e-hentai.org" "e-hentai.org"
}
override val baseUrl: String override val baseUrl: String
get() = "$schema://$domain" get() = "$schema://$domain"
@ -111,25 +113,27 @@ class EHentai(
val favElement = column2.children().find { it.attr("style").startsWith("border-color") } val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
ParsedManga( ParsedManga(
fav = FAVORITES_BORDER_HEX_COLORS.indexOf( fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
favElement?.attr("style")?.substring(14, 17) favElement?.attr("style")?.substring(14, 17)
), ),
manga = Manga.create(id).apply { manga = Manga.create(id).apply {
// Get title // Get title
title = thumbnailElement.attr("title") title = thumbnailElement.attr("title")
url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href")) url = EHentaiSearchMetadata.normalizeUrl(linkElement.attr("href"))
// Get image // Get image
thumbnail_url = thumbnailElement.attr("src") thumbnail_url = thumbnailElement.attr("src")
// TODO Parse genre + uploader + tags // TODO Parse genre + uploader + tags
}) }
)
} }
val parsedLocation = doc.location().toHttpUrlOrNull() val parsedLocation = doc.location().toHttpUrlOrNull()
// Add to page if required // Add to page if required
val hasNextPage = if (parsedLocation == null || val hasNextPage = if (parsedLocation == null ||
!parsedLocation.queryParameterNames.contains(REVERSE_PARAM)) { !parsedLocation.queryParameterNames.contains(REVERSE_PARAM)
) {
select("a[onclick=return false]").last()?.let { select("a[onclick=return false]").last()?.let {
it.text() == ">" it.text() == ">"
} ?: false } ?: false
@ -160,7 +164,7 @@ class EHentai(
while (true) { while (true) {
val gid = EHentaiSearchMetadata.galleryId(url).toInt() val gid = EHentaiSearchMetadata.galleryId(url).toInt()
val cachedParent = updateHelper.parentLookupTable.get( val cachedParent = updateHelper.parentLookupTable.get(
gid gid
) )
if (cachedParent == null) { if (cachedParent == null) {
throttleFunc() throttleFunc()
@ -175,19 +179,19 @@ class EHentai(
if (parentLink != null) { if (parentLink != null) {
updateHelper.parentLookupTable.put( updateHelper.parentLookupTable.put(
gid, gid,
GalleryEntry( GalleryEntry(
EHentaiSearchMetadata.galleryId(parentLink), EHentaiSearchMetadata.galleryId(parentLink),
EHentaiSearchMetadata.galleryToken(parentLink) EHentaiSearchMetadata.galleryToken(parentLink)
) )
) )
url = EHentaiSearchMetadata.normalizeUrl(parentLink) url = EHentaiSearchMetadata.normalizeUrl(parentLink)
} else break } else break
} else { } else {
XLog.d("Parent cache hit: %s!", gid) XLog.d("Parent cache hit: %s!", gid)
url = EHentaiSearchMetadata.idAndTokenToUrl( url = EHentaiSearchMetadata.idAndTokenToUrl(
cachedParent.gId, cachedParent.gId,
cachedParent.gToken cachedParent.gToken
) )
} }
} }
@ -201,9 +205,11 @@ class EHentai(
url = EHentaiSearchMetadata.normalizeUrl(d.location()) url = EHentaiSearchMetadata.normalizeUrl(d.location())
name = "v1: " + d.selectFirst("#gn").text() name = "v1: " + d.selectFirst("#gn").text()
chapter_number = 1f chapter_number = 1f
date_upload = EX_DATE_FORMAT.parse(d.select("#gdd .gdt1").find { el -> date_upload = EX_DATE_FORMAT.parse(
el.text().toLowerCase() == "posted:" d.select("#gdd .gdt1").find { el ->
}!!.nextElementSibling().text()).time el.text().toLowerCase() == "posted:"
}!!.nextElementSibling().text()
).time
} }
// Build and append the rest of the galleries // Build and append the rest of the galleries
if (DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) listOf(self) if (DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) listOf(self)
@ -253,27 +259,32 @@ class EHentai(
}.sortedBy(Pair<Int, String>::first).map { it.second } }.sortedBy(Pair<Int, String>::first).map { it.second }
} }
private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess() private fun chapterPageCall(np: String): Observable<Response> {
private fun chapterPageRequest(np: String) = exGet(np, null, headers) return client.newCall(chapterPageRequest(np)).asObservableSuccess()
}
private fun chapterPageRequest(np: String): Request {
return exGet(np, null, headers)
}
private fun nextPageUrl(element: Element): String? = element.select("a[onclick=return false]").last()?.let { private fun nextPageUrl(element: Element): String? = element.select("a[onclick=return false]").last()?.let {
return if (it.text() == ">") it.attr("href") else null return if (it.text() == ">") it.attr("href") else null
} }
override fun popularMangaRequest(page: Int) = if (exh) override fun popularMangaRequest(page: Int) = if (exh) {
latestUpdatesRequest(page) latestUpdatesRequest(page)
else } else {
exGet("$baseUrl/toplist.php?tl=15&p=${page - 1}", null) // Custom page logic for toplists exGet("$baseUrl/toplist.php?tl=15&p=${page - 1}", null) // Custom page logic for toplists
}
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(query) {
searchMangaRequestObservable(page, query, filters).flatMap { searchMangaRequestObservable(page, query, filters).flatMap {
client.newCall(it).asObservableSuccess() client.newCall(it).asObservableSuccess()
}.map { response -> }.map { response ->
searchMangaParse(response) searchMangaParse(response)
}
} }
}
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> { private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon() val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon()
@ -287,20 +298,20 @@ class EHentai(
// Reverse search results on filter // Reverse search results on filter
if (filters.any { it is ReverseFilter && it.state }) { if (filters.any { it is ReverseFilter && it.state }) {
return client.newCall(request) return client.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.map { .map {
val doc = it.asJsoup() val doc = it.asJsoup()
val elements = doc.select(".ptt > tbody > tr > td") val elements = doc.select(".ptt > tbody > tr > td")
val totalElement = elements[elements.size - 2] val totalElement = elements[elements.size - 2]
val thisPage = totalElement.text().toInt() - (page - 1) val thisPage = totalElement.text().toInt() - (page - 1)
uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString()) uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString())
exGet(uri.toString(), thisPage) exGet(uri.toString(), thisPage)
} }
} else { } else {
return Observable.just(request) return Observable.just(request)
} }
@ -314,22 +325,28 @@ class EHentai(
override fun searchMangaParse(response: Response) = genericMangaParse(response) override fun searchMangaParse(response: Response) = genericMangaParse(response)
override fun latestUpdatesParse(response: Response) = genericMangaParse(response) override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true) = GET(page?.let { fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
addParam(url, "page", Integer.toString(page - 1)) return GET(
} ?: url, additionalHeaders?.let { page?.let {
val headers = headers.newBuilder() addParam(url, "page", Integer.toString(page - 1))
it.toMultimap().forEach { (t, u) -> } ?: url,
u.forEach { additionalHeaders?.let {
headers.add(t, it) val headers = headers.newBuilder()
it.toMultimap().forEach { (t, u) ->
u.forEach {
headers.add(t, it)
}
}
headers.build()
} ?: headers
).let {
if (!cache) {
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
} else {
it
} }
} }
headers.build() }
} ?: headers).let {
if (!cache)
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
else
it
}!!
/** /**
* Returns an observable with the updated details for a manga. Normally it's not needed to * Returns an observable with the updated details for a manga. Normally it's not needed to
@ -339,33 +356,37 @@ class EHentai(
*/ */
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableWithAsyncStacktrace() .asObservableWithAsyncStacktrace()
.flatMap { (stacktrace, response) -> .flatMap { (stacktrace, response) ->
if (response.isSuccessful) { if (response.isSuccessful) {
// Pull to most recent // Pull to most recent
val doc = response.asJsoup() val doc = response.asJsoup()
val newerGallery = doc.select("#gnd a").lastOrNull() val newerGallery = doc.select("#gnd a").lastOrNull()
val pre = if (newerGallery != null && DebugToggles.PULL_TO_ROOT_WHEN_LOADING_EXH_MANGA_DETAILS.enabled) { val pre = if (newerGallery != null && DebugToggles.PULL_TO_ROOT_WHEN_LOADING_EXH_MANGA_DETAILS.enabled) {
manga.url = EHentaiSearchMetadata.normalizeUrl(newerGallery.attr("href")) manga.url = EHentaiSearchMetadata.normalizeUrl(newerGallery.attr("href"))
client.newCall(mangaDetailsRequest(manga)) client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess().map { it.asJsoup() } .asObservableSuccess().map { it.asJsoup() }
} else Observable.just(doc) } else Observable.just(doc)
pre.flatMap { pre.flatMap {
parseToManga(manga, it).andThen(Observable.just(manga.apply { parseToManga(manga, it).andThen(
initialized = true Observable.just(
})) manga.apply {
} initialized = true
}
)
)
}
} else {
response.close()
if (response.code == 404) {
throw GalleryNotFoundException(stacktrace)
} else { } else {
response.close() throw Exception("HTTP error ${response.code}", stacktrace)
if (response.code == 404) {
throw GalleryNotFoundException(stacktrace)
} else {
throw Exception("HTTP error ${response.code}", stacktrace)
}
} }
} }
}
} }
/** /**
@ -389,11 +410,11 @@ class EHentai(
it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')')) it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
} }
genre = select(".cs") genre = select(".cs")
.attr("onclick") .attr("onclick")
.nullIfBlank() .nullIfBlank()
?.trim() ?.trim()
?.substringAfterLast('/') ?.substringAfterLast('/')
?.removeSuffix("'") ?.removeSuffix("'")
uploader = select("#gdn").text().nullIfBlank()?.trim() uploader = select("#gdn").text().nullIfBlank()?.trim()
@ -404,8 +425,10 @@ class EHentai(
val right = rightElement.text().nullIfBlank()?.trim() val right = rightElement.text().nullIfBlank()?.trim()
if (left != null && right != null) { if (left != null && right != null) {
ignore { ignore {
when (left.removeSuffix(":") when (
.toLowerCase()) { left.removeSuffix(":")
.toLowerCase()
) {
"posted" -> datePosted = EX_DATE_FORMAT.parse(right).time "posted" -> datePosted = EX_DATE_FORMAT.parse(right).time
// Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/ // Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/
// Example JP gallery: https://exhentai.org/g/1375385/03519d541b/ // Example JP gallery: https://exhentai.org/g/1375385/03519d541b/
@ -428,7 +451,8 @@ class EHentai(
lastUpdateCheck = System.currentTimeMillis() lastUpdateCheck = System.currentTimeMillis()
if (datePosted != null && if (datePosted != null &&
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME) { lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME
) {
aged = true aged = true
XLog.d("aged %s - too old", title) XLog.d("aged %s - too old", title)
} }
@ -436,32 +460,35 @@ class EHentai(
// Parse ratings // Parse ratings
ignore { ignore {
averageRating = select("#rating_label") averageRating = select("#rating_label")
.text() .text()
.removePrefix("Average:") .removePrefix("Average:")
.trim() .trim()
.nullIfBlank() .nullIfBlank()
?.toDouble() ?.toDouble()
ratingCount = select("#rating_count") ratingCount = select("#rating_count")
.text() .text()
.trim() .trim()
.nullIfBlank() .nullIfBlank()
?.toInt() ?.toInt()
} }
// Parse tags // Parse tags
tags.clear() tags.clear()
select("#taglist tr").forEach { select("#taglist tr").forEach {
val namespace = it.select(".tc").text().removeSuffix(":") val namespace = it.select(".tc").text().removeSuffix(":")
tags.addAll(it.select("div").map { element -> tags.addAll(
RaisedTag( it.select("div").map { element ->
RaisedTag(
namespace, namespace,
element.text().trim(), element.text().trim(),
if (element.hasClass("gtl")) if (element.hasClass("gtl")) {
TAG_TYPE_LIGHT TAG_TYPE_LIGHT
else } else {
TAG_TYPE_NORMAL TAG_TYPE_NORMAL
) }
}) )
}
)
} }
// Add genre as virtual tag // Add genre as virtual tag
@ -478,8 +505,8 @@ class EHentai(
override fun fetchImageUrl(page: Page): Observable<String> { override fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page)) return client.newCall(imageUrlRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { realImageUrlParse(it, page) } .map { realImageUrlParse(it, page) }
} }
fun realImageUrlParse(response: Response, page: Page): String { fun realImageUrlParse(response: Response, page: Page): String {
@ -505,9 +532,13 @@ class EHentai(
var favNames: List<String>? = null var favNames: List<String>? = null
do { do {
val response2 = client.newCall(exGet(favoriteUrl, val response2 = client.newCall(
exGet(
favoriteUrl,
page = page, page = page,
cache = false)).execute() cache = false
)
).execute()
val doc = response2.asJsoup() val doc = response2.asJsoup()
// Parse favorites // Parse favorites
@ -515,22 +546,24 @@ class EHentai(
result += parsed.first result += parsed.first
// Parse fav names // Parse fav names
if (favNames == null) if (favNames == null) {
favNames = doc.select(".fp:not(.fps)").mapNotNull { favNames = doc.select(".fp:not(.fps)").mapNotNull {
it.child(2).text() it.child(2).text()
} }
}
// Next page // Next page
page++ page++
} while (parsed.second) } while (parsed.second)
return Pair(result as List<ParsedManga>, favNames!!) return Pair(result as List<ParsedManga>, favNames!!)
} }
fun spPref() = if (exh) fun spPref() = if (exh) {
prefs.eh_exhSettingsProfile() prefs.eh_exhSettingsProfile()
else } else {
prefs.eh_ehSettingsProfile() prefs.eh_ehSettingsProfile()
}
fun rawCookies(sp: Int): Map<String, String> { fun rawCookies(sp: Int): Map<String, String> {
val cookies: MutableMap<String, String> = mutableMapOf() val cookies: MutableMap<String, String> = mutableMapOf()
@ -541,16 +574,19 @@ class EHentai(
cookies["sp"] = sp.toString() cookies["sp"] = sp.toString()
val sessionKey = prefs.eh_settingsKey().getOrDefault() val sessionKey = prefs.eh_settingsKey().getOrDefault()
if (sessionKey != null) if (sessionKey != null) {
cookies["sk"] = sessionKey cookies["sk"] = sessionKey
}
val sessionCookie = prefs.eh_sessionCookie().getOrDefault() val sessionCookie = prefs.eh_sessionCookie().getOrDefault()
if (sessionCookie != null) if (sessionCookie != null) {
cookies["s"] = sessionCookie cookies["s"] = sessionCookie
}
val hathPerksCookie = prefs.eh_hathPerksCookies().getOrDefault() val hathPerksCookie = prefs.eh_hathPerksCookies().getOrDefault()
if (hathPerksCookie != null) if (hathPerksCookie != null) {
cookies["hath_perks"] = hathPerksCookie cookies["hath_perks"] = hathPerksCookie
}
} }
// Session-less extended display mode (for users without ExHentai) // Session-less extended display mode (for users without ExHentai)
@ -568,51 +604,57 @@ class EHentai(
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())!! override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())!!
fun addParam(url: String, param: String, value: String) = Uri.parse(url) fun addParam(url: String, param: String, value: String) = Uri.parse(url)
.buildUpon() .buildUpon()
.appendQueryParameter(param, value) .appendQueryParameter(param, value)
.toString() .toString()
override val client = network.client.newBuilder() override val client = network.client.newBuilder()
.cookieJar(CookieJar.NO_COOKIES) .cookieJar(CookieJar.NO_COOKIES)
.addInterceptor { chain -> .addInterceptor { chain ->
val newReq = chain val newReq = chain
.request() .request()
.newBuilder() .newBuilder()
.removeHeader("Cookie") .removeHeader("Cookie")
.addHeader("Cookie", cookiesHeader()) .addHeader("Cookie", cookiesHeader())
.build() .build()
chain.proceed(newReq) chain.proceed(newReq)
}.build()!! }.build()!!
// Filters // Filters
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
Watched(), Watched(),
GenreGroup(), GenreGroup(),
AdvancedGroup(), AdvancedGroup(),
ReverseFilter() ReverseFilter()
) )
class Watched : Filter.CheckBox("Watched List"), UriFilter { class Watched : Filter.CheckBox("Watched List"), UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
if (state) if (state) {
builder.appendPath("watched") builder.appendPath("watched")
}
} }
} }
class GenreOption(name: String, val genreId: Int) : Filter.CheckBox(name, false) class GenreOption(name: String, val genreId: Int) : Filter.CheckBox(name, false)
class GenreGroup : Filter.Group<GenreOption>("Genres", listOf( class GenreGroup :
GenreOption("Dōjinshi", 2), Filter.Group<GenreOption>(
GenreOption("Manga", 4), "Genres",
GenreOption("Artist CG", 8), listOf(
GenreOption("Game CG", 16), GenreOption("Dōjinshi", 2),
GenreOption("Western", 512), GenreOption("Manga", 4),
GenreOption("Non-H", 256), GenreOption("Artist CG", 8),
GenreOption("Image Set", 32), GenreOption("Game CG", 16),
GenreOption("Cosplay", 64), GenreOption("Western", 512),
GenreOption("Asian Porn", 128), GenreOption("Non-H", 256),
GenreOption("Misc", 1) GenreOption("Image Set", 32),
)), UriFilter { GenreOption("Cosplay", 64),
GenreOption("Asian Porn", 128),
GenreOption("Misc", 1)
)
),
UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
val bits = state.fold(0) { acc, genre -> val bits = state.fold(0) { acc, genre ->
if (!genre.state) acc + genre.genreId else acc if (!genre.state) acc + genre.genreId else acc
@ -623,8 +665,9 @@ class EHentai(
class AdvancedOption(name: String, val param: String, defValue: Boolean = false) : Filter.CheckBox(name, defValue), UriFilter { class AdvancedOption(name: String, val param: String, defValue: Boolean = false) : Filter.CheckBox(name, defValue), UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
if (state) if (state) {
builder.appendQueryParameter(param, "on") builder.appendQueryParameter(param, "on")
}
} }
} }
@ -643,13 +686,18 @@ class EHentai(
class MinPagesOption : PageOption("Minimum Pages", "f_spf") class MinPagesOption : PageOption("Minimum Pages", "f_spf")
class MaxPagesOption : PageOption("Maximum Pages", "f_spt") class MaxPagesOption : PageOption("Maximum Pages", "f_spt")
class RatingOption : Filter.Select<String>("Minimum Rating", arrayOf( class RatingOption :
"Any", Filter.Select<String>(
"2 stars", "Minimum Rating",
"3 stars", arrayOf(
"4 stars", "Any",
"5 stars" "2 stars",
)), UriFilter { "3 stars",
"4 stars",
"5 stars"
)
),
UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
if (state > 0) { if (state > 0) {
builder.appendQueryParameter("f_srdd", Integer.toString(state + 1)) builder.appendQueryParameter("f_srdd", Integer.toString(state + 1))
@ -658,7 +706,9 @@ class EHentai(
} }
} }
class AdvancedGroup : UriGroup<Filter<*>>("Advanced Options", listOf( class AdvancedGroup : UriGroup<Filter<*>>(
"Advanced Options",
listOf(
AdvancedOption("Search Gallery Name", "f_sname", true), AdvancedOption("Search Gallery Name", "f_sname", true),
AdvancedOption("Search Gallery Tags", "f_stags", true), AdvancedOption("Search Gallery Tags", "f_stags", true),
AdvancedOption("Search Gallery Description", "f_sdesc"), AdvancedOption("Search Gallery Description", "f_sdesc"),
@ -670,24 +720,26 @@ class EHentai(
RatingOption(), RatingOption(),
MinPagesOption(), MinPagesOption(),
MaxPagesOption() MaxPagesOption()
)) )
)
class ReverseFilter : Filter.CheckBox("Reverse search results") class ReverseFilter : Filter.CheckBox("Reverse search results")
override val name = if (exh) override val name = if (exh) {
"ExHentai" "ExHentai"
else } else {
"E-Hentai" "E-Hentai"
}
class GalleryNotFoundException(cause: Throwable) : RuntimeException("Gallery not found!", cause) class GalleryNotFoundException(cause: Throwable) : RuntimeException("Gallery not found!", cause)
// === URL IMPORT STUFF // === URL IMPORT STUFF
override val matchingHosts: List<String> = if (exh) listOf( override val matchingHosts: List<String> = if (exh) listOf(
"exhentai.org" "exhentai.org"
) else listOf( ) else listOf(
"g.e-hentai.org", "g.e-hentai.org",
"e-hentai.org" "e-hentai.org"
) )
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {
@ -717,17 +769,23 @@ class EHentai(
val json = JsonObject() val json = JsonObject()
json["method"] = "gtoken" json["method"] = "gtoken"
json["pagelist"] = JsonArray().apply { json["pagelist"] = JsonArray().apply {
add(JsonArray().apply { add(
add(gallery.toInt()) JsonArray().apply {
add(pageToken) add(gallery.toInt())
add(pageNum.toInt()) add(pageToken)
}) add(pageNum.toInt())
}
)
} }
val outJson = JsonParser.parseString(client.newCall(Request.Builder() val outJson = JsonParser.parseString(
.url(EH_API_BASE) client.newCall(
.post(RequestBody.create(JSON, json.toString())) Request.Builder()
.build()).execute().body!!.string()).obj .url(EH_API_BASE)
.post(RequestBody.create(JSON, json.toString()))
.build()
).execute().body!!.string()
).obj
val obj = outJson["tokenlist"].array.first() val obj = outJson["tokenlist"].array.first()
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/" return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
@ -742,16 +800,16 @@ class EHentai(
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!! private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
private val FAVORITES_BORDER_HEX_COLORS = listOf( private val FAVORITES_BORDER_HEX_COLORS = listOf(
"000", "000",
"f00", "f00",
"fa0", "fa0",
"dd0", "dd0",
"080", "080",
"9f4", "9f4",
"4bf", "4bf",
"00f", "00f",
"508", "508",
"e8e" "e8e"
) )
fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ") { fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ") {

View File

@ -65,7 +65,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
private fun tagIndexVersion(): Single<Long> { private fun tagIndexVersion(): Single<Long> {
val sCachedTagIndexVersion = cachedTagIndexVersion val sCachedTagIndexVersion = cachedTagIndexVersion
return if (sCachedTagIndexVersion == null || return if (sCachedTagIndexVersion == null ||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) { tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
) {
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext { HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
cachedTagIndexVersion = it cachedTagIndexVersion = it
tagIndexVersionCacheTime = System.currentTimeMillis() tagIndexVersionCacheTime = System.currentTimeMillis()
@ -80,7 +81,8 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
private fun galleryIndexVersion(): Single<Long> { private fun galleryIndexVersion(): Single<Long> {
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
return if (sCachedGalleryIndexVersion == null || return if (sCachedGalleryIndexVersion == null ||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()) { galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
) {
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext { HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
cachedGalleryIndexVersion = it cachedGalleryIndexVersion = it
galleryIndexVersionCacheTime = System.currentTimeMillis() galleryIndexVersionCacheTime = System.currentTimeMillis()
@ -162,9 +164,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet( override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet(
"$LTN_BASE_URL/popular-all.nozomi", "$LTN_BASE_URL/popular-all.nozomi",
100L * (page - 1), 100L * (page - 1),
99L + 100 * (page - 1) 99L + 100 * (page - 1)
) )
/** /**
@ -192,7 +194,7 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
// TODO Cache the results coming out of HitomiNozomi // TODO Cache the results coming out of HitomiNozomi
val hn = Single.zip(tagIndexVersion(), galleryIndexVersion()) { tv, gv -> tv to gv } val hn = Single.zip(tagIndexVersion(), galleryIndexVersion()) { tv, gv -> tv to gv }
.map { HitomiNozomi(client, it.first, it.second) } .map { HitomiNozomi(client, it.first, it.second) }
var base = if (positive.isEmpty()) { var base = if (positive.isEmpty()) {
hn.flatMap { n -> n.getGalleryIdsFromNozomi(null, "index", "all").map { n to it.toSet() } } hn.flatMap { n -> n.getGalleryIdsFromNozomi(null, "index", "all").map { n to it.toSet() } }
@ -240,9 +242,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun latestUpdatesRequest(page: Int) = HitomiNozomi.rangedGet( override fun latestUpdatesRequest(page: Int) = HitomiNozomi.rangedGet(
"$LTN_BASE_URL/index-all.nozomi", "$LTN_BASE_URL/index-all.nozomi",
100L * (page - 1), 100L * (page - 1),
99L + 100 * (page - 1) 99L + 100 * (page - 1)
) )
/** /**
@ -254,14 +256,14 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page)) return client.newCall(popularMangaRequest(page))
.asObservableSuccess() .asObservableSuccess()
.flatMap { responseToMangas(it) } .flatMap { responseToMangas(it) }
} }
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newCall(latestUpdatesRequest(page)) return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess() .asObservableSuccess()
.flatMap { responseToMangas(it) } .flatMap { responseToMangas(it) }
} }
fun responseToMangas(response: Response): Observable<MangasPage> { fun responseToMangas(response: Response): Observable<MangasPage> {
@ -270,9 +272,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
val end = range.substringBefore('/').substringAfter('-').toLong() val end = range.substringBefore('/').substringAfter('-').toLong()
val body = response.body!! val body = response.body!!
return parseNozomiPage(body.bytes()) return parseNozomiPage(body.bytes())
.map { .map {
MangasPage(it, end < total - 1) MangasPage(it, end < total - 1)
} }
} }
private fun parseNozomiPage(array: ByteArray): Observable<List<SManga>> { private fun parseNozomiPage(array: ByteArray): Observable<List<SManga>> {
@ -285,13 +287,15 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
} }
private fun nozomiIdsToMangas(ids: List<Int>): Single<List<SManga>> { private fun nozomiIdsToMangas(ids: List<Int>): Single<List<SManga>> {
return Single.zip(ids.map { return Single.zip(
client.newCall(GET("$LTN_BASE_URL/galleryblock/$it.html")) ids.map {
client.newCall(GET("$LTN_BASE_URL/galleryblock/$it.html"))
.asObservableSuccess() .asObservableSuccess()
.subscribeOn(Schedulers.io()) // Perform all these requests in parallel .subscribeOn(Schedulers.io()) // Perform all these requests in parallel
.map { parseGalleryBlock(it) } .map { parseGalleryBlock(it) }
.toSingle() .toSingle()
}) { it.map { m -> m as SManga } } }
) { it.map { m -> m as SManga } }
} }
private fun parseGalleryBlock(response: Response): SManga { private fun parseGalleryBlock(response: Response): SManga {
@ -318,23 +322,27 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
*/ */
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply { parseToManga(manga, it.asJsoup()).andThen(
initialized = true Observable.just(
})) manga.apply {
} initialized = true
}
)
)
}
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just( return Observable.just(
listOf( listOf(
SChapter.create().apply { SChapter.create().apply {
url = manga.url url = manga.url
name = "Chapter" name = "Chapter"
chapter_number = 0.0f chapter_number = 0.0f
} }
) )
) )
} }
@ -372,9 +380,9 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
val hashPath1 = hash.takeLast(1) val hashPath1 = hash.takeLast(1)
val hashPath2 = hash.takeLast(3).take(2) val hashPath2 = hash.takeLast(3).take(2)
Page( Page(
index, index,
"", "",
"https://${subdomainFromGalleryId(hlId)}a.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext" "https://${subdomainFromGalleryId(hlId)}a.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext"
) )
} }
} }
@ -396,19 +404,20 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
it[it.lastIndex - 1] it[it.lastIndex - 1]
} }
return request.newBuilder() return request.newBuilder()
.header("Referer", "$BASE_URL/reader/$hlId.html") .header("Referer", "$BASE_URL/reader/$hlId.html")
.build() .build()
} }
override val matchingHosts = listOf( override val matchingHosts = listOf(
"hitomi.la" "hitomi.la"
) )
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
if (lcFirstPathSegment != "manga" && lcFirstPathSegment != "reader") if (lcFirstPathSegment != "manga" && lcFirstPathSegment != "reader") {
return null return null
}
return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html" return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html"
} }
@ -419,10 +428,11 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
private val NUMBER_OF_FRONTENDS = 2 private val NUMBER_OF_FRONTENDS = 2
private val DATE_FORMAT by lazy { private val DATE_FORMAT by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US) SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US)
else } else {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss'-05'", Locale.US) SimpleDateFormat("yyyy-MM-dd HH:mm:ss'-05'", Locale.US)
}
} }
} }
} }

View File

@ -80,7 +80,7 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
} }
val sortFilter = filters.filterIsInstance<SortFilter>().firstOrNull()?.state val sortFilter = filters.filterIsInstance<SortFilter>().firstOrNull()?.state
?: defaultSortFilterSelection() ?: defaultSortFilterSelection()
if (sortFilter.index == 1) { if (sortFilter.index == 1) {
if (query.isBlank()) error("You must specify a search query if you wish to sort by popularity!") if (query.isBlank()) error("You must specify a search query if you wish to sort by popularity!")
@ -89,22 +89,22 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
if (sortFilter.ascending) { if (sortFilter.ascending) {
return client.newCall(nhGet(uri.toString())) return client.newCall(nhGet(uri.toString()))
.asObservableSuccess() .asObservableSuccess()
.map { .map {
val doc = it.asJsoup() val doc = it.asJsoup()
val lastPage = doc.selectFirst(".last") val lastPage = doc.selectFirst(".last")
?.attr("href") ?.attr("href")
?.substringAfterLast('=') ?.substringAfterLast('=')
?.toIntOrNull() ?: 1 ?.toIntOrNull() ?: 1
val thisPage = lastPage - (page - 1) val thisPage = lastPage - (page - 1)
uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString()) uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString())
uri.appendQueryParameter("page", thisPage.toString()) uri.appendQueryParameter("page", thisPage.toString())
nhGet(uri.toString(), page) nhGet(uri.toString(), page)
} }
} }
uri.appendQueryParameter("page", page.toString()) uri.appendQueryParameter("page", page.toString())
@ -134,12 +134,16 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
*/ */
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
parseToManga(manga, it).andThen(Observable.just(manga.apply { parseToManga(manga, it).andThen(
initialized = true Observable.just(
})) manga.apply {
} initialized = true
}
)
)
}
} }
override fun mangaDetailsRequest(manga: SManga) = nhGet(baseUrl + manga.url) override fun mangaDetailsRequest(manga: SManga) = nhGet(baseUrl + manga.url)
@ -208,31 +212,38 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
}?.apply { }?.apply {
tags.clear() tags.clear()
}?.forEach { }?.forEach {
if (it.first != null && it.second != null) if (it.first != null && it.second != null) {
tags.add(RaisedTag(it.first!!, it.second!!, TAG_TYPE_DEFAULT)) tags.add(RaisedTag(it.first!!, it.second!!, TAG_TYPE_DEFAULT))
}
} }
} }
} }
fun getOrLoadMetadata(mangaId: Long?, nhId: Long) = getOrLoadMetadata(mangaId) { fun getOrLoadMetadata(mangaId: Long?, nhId: Long) = getOrLoadMetadata(mangaId) {
client.newCall(nhGet(baseUrl + NHentaiSearchMetadata.nhIdToPath(nhId))) client.newCall(nhGet(baseUrl + NHentaiSearchMetadata.nhIdToPath(nhId)))
.asObservableSuccess() .asObservableSuccess()
.toSingle() .toSingle()
} }
override fun fetchChapterList(manga: SManga) = Observable.just(listOf(SChapter.create().apply { override fun fetchChapterList(manga: SManga) = Observable.just(
url = manga.url listOf(
name = "Chapter" SChapter.create().apply {
chapter_number = 1f url = manga.url
})) name = "Chapter"
chapter_number = 1f
}
)
)
override fun fetchPageList(chapter: SChapter) = getOrLoadMetadata(chapter.mangaId, NHentaiSearchMetadata.nhUrlToId(chapter.url)).map { metadata -> override fun fetchPageList(chapter: SChapter) = getOrLoadMetadata(chapter.mangaId, NHentaiSearchMetadata.nhUrlToId(chapter.url)).map { metadata ->
if (metadata.mediaId == null) emptyList() if (metadata.mediaId == null) {
else emptyList()
} else {
metadata.pageImageTypes.mapIndexed { index, s -> metadata.pageImageTypes.mapIndexed { index, s ->
val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s) val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s)
Page(index, imageUrl!!, imageUrl) Page(index, imageUrl!!, imageUrl)
} }
}
}.toObservable() }.toObservable()
override fun fetchImageUrl(page: Page) = Observable.just(page.imageUrl!!)!! override fun fetchImageUrl(page: Page) = Observable.just(page.imageUrl!!)!!
@ -259,9 +270,9 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
private class filterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray()) private class filterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
class SortFilter : Filter.Sort( class SortFilter : Filter.Sort(
"Sort", "Sort",
arrayOf("Date", "Popular"), arrayOf("Date", "Popular"),
defaultSortFilterSelection() defaultSortFilterSelection()
) )
val appName by lazy { val appName by lazy {
@ -269,14 +280,16 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
} }
fun nhGet(url: String, tag: Any? = null) = GET(url) fun nhGet(url: String, tag: Any? = null) = GET(url)
.newBuilder() .newBuilder()
.header("User-Agent", .header(
"Mozilla/5.0 (X11; Linux x86_64) " + "User-Agent",
"AppleWebKit/537.36 (KHTML, like Gecko) " + "Mozilla/5.0 (X11; Linux x86_64) " +
"Chrome/56.0.2924.87 " + "AppleWebKit/537.36 (KHTML, like Gecko) " +
"Safari/537.36 " + "Chrome/56.0.2924.87 " +
"$appName/${BuildConfig.VERSION_CODE}") "Safari/537.36 " +
.tag(tag).build() "$appName/${BuildConfig.VERSION_CODE}"
)
.tag(tag).build()
override val id = NHENTAI_SOURCE_ID override val id = NHENTAI_SOURCE_ID
@ -291,12 +304,13 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
// === URL IMPORT STUFF // === URL IMPORT STUFF
override val matchingHosts = listOf( override val matchingHosts = listOf(
"nhentai.net" "nhentai.net"
) )
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {
if (uri.pathSegments.firstOrNull()?.toLowerCase() != "g") if (uri.pathSegments.firstOrNull()?.toLowerCase() != "g") {
return null return null
}
return "$baseUrl/g/${uri.pathSegments[1]}/" return "$baseUrl/g/${uri.pathSegments[1]}/"
} }
@ -308,10 +322,10 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
private fun defaultSortFilterSelection() = Filter.Sort.Selection(0, false) private fun defaultSortFilterSelection() = Filter.Sort.Selection(0, false)
private val SOURCE_LANG_LIST = listOf( private val SOURCE_LANG_LIST = listOf(
Pair("All", ""), Pair("All", ""),
Pair("English", " english"), Pair("English", " english"),
Pair("Japanese", " japanese"), Pair("Japanese", " japanese"),
Pair("Chinese", " chinese") Pair("Chinese", " chinese")
) )
} }
} }

View File

@ -33,8 +33,10 @@ import org.jsoup.nodes.TextNode
import rx.Observable import rx.Observable
// TODO Transform into delegated source // TODO Transform into delegated source
class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSource(), class PervEden(override val id: Long, val pvLang: PervEdenLang) :
LewdSource<PervEdenSearchMetadata, Document>, UrlImportableSource { ParsedHttpSource(),
LewdSource<PervEdenSearchMetadata, Document>,
UrlImportableSource {
/** /**
* The class of the metadata used by this source * The class of the metadata used by this source
*/ */
@ -62,9 +64,9 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun searchMangaSelector() = "#mangaList > tbody > tr" override fun searchMangaSelector() = "#mangaList > tbody > tr"
@ -79,9 +81,11 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
override fun searchMangaNextPageSelector() = ".next" override fun searchMangaNextPageSelector() = ".next"
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val urlLang = if (lang == "en") val urlLang = if (lang == "en") {
"eng" "eng"
else "it" } else {
"it"
}
return GET("$baseUrl/$urlLang/") return GET("$baseUrl/$urlLang/")
} }
@ -129,12 +133,16 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
*/ */
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply { parseToManga(manga, it.asJsoup()).andThen(
initialized = true Observable.just(
})) manga.apply {
} initialized = true
}
)
)
}
} }
/** /**
@ -165,8 +173,9 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
"Alternative name(s)" -> { "Alternative name(s)" -> {
if (it is TextNode) { if (it is TextNode) {
val text = it.text().trim() val text = it.text().trim()
if (!text.isBlank()) if (!text.isBlank()) {
newAltTitles += text newAltTitles += text
}
} }
} }
"Artist" -> { "Artist" -> {
@ -176,21 +185,24 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
} }
} }
"Genres" -> { "Genres" -> {
if (it is Element && it.tagName() == "a") if (it is Element && it.tagName() == "a") {
tags += RaisedTag(null, it.text().toLowerCase(), TAG_TYPE_DEFAULT) tags += RaisedTag(null, it.text().toLowerCase(), TAG_TYPE_DEFAULT)
}
} }
"Type" -> { "Type" -> {
if (it is TextNode) { if (it is TextNode) {
val text = it.text().trim() val text = it.text().trim()
if (!text.isBlank()) if (!text.isBlank()) {
type = text type = text
}
} }
} }
"Status" -> { "Status" -> {
if (it is TextNode) { if (it is TextNode) {
val text = it.text().trim() val text = it.text().trim()
if (!text.isBlank()) if (!text.isBlank()) {
status = text status = text
}
} }
} }
} }
@ -224,10 +236,11 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
name = "Chapter " + linkElement.getElementsByTag("b").text() name = "Chapter " + linkElement.getElementsByTag("b").text()
ChapterRecognition.parseChapterNumber( ChapterRecognition.parseChapterNumber(
this, this,
SManga.create().apply { SManga.create().apply {
title = "" title = ""
}) }
)
try { try {
date_upload = DATE_FORMAT.parse(element.getElementsByClass("chapterDate").first().text().trim())!!.time date_upload = DATE_FORMAT.parse(element.getElementsByClass("chapterDate").first().text().trim())!!.time
@ -242,37 +255,49 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
override fun imageUrlParse(document: Document) = "http:" + document.getElementById("mainImg").attr("src")!! override fun imageUrlParse(document: Document) = "http:" + document.getElementById("mainImg").attr("src")!!
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
AuthorFilter(), AuthorFilter(),
ArtistFilter(), ArtistFilter(),
TypeFilterGroup(), TypeFilterGroup(),
ReleaseYearGroup(), ReleaseYearGroup(),
StatusFilterGroup() StatusFilterGroup()
) )
class StatusFilterGroup : UriGroup<StatusFilter>("Status", listOf( class StatusFilterGroup : UriGroup<StatusFilter>(
"Status",
listOf(
StatusFilter("Ongoing", 1), StatusFilter("Ongoing", 1),
StatusFilter("Completed", 2), StatusFilter("Completed", 2),
StatusFilter("Suspended", 0) StatusFilter("Suspended", 0)
)) )
)
class StatusFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter { class StatusFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
if (state) if (state) {
builder.appendQueryParameter("status", id.toString()) builder.appendQueryParameter("status", id.toString())
}
} }
} }
// Explicit type arg for listOf() to workaround this: KT-16570 // Explicit type arg for listOf() to workaround this: KT-16570
class ReleaseYearGroup : UriGroup<Filter<*>>("Release Year", listOf( class ReleaseYearGroup : UriGroup<Filter<*>>(
"Release Year",
listOf(
ReleaseYearRangeFilter(), ReleaseYearRangeFilter(),
ReleaseYearYearFilter() ReleaseYearYearFilter()
)) )
)
class ReleaseYearRangeFilter : Filter.Select<String>("Range", arrayOf( class ReleaseYearRangeFilter :
"on", Filter.Select<String>(
"after", "Range",
"before" arrayOf(
)), UriFilter { "on",
"after",
"before"
)
),
UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("releasedType", state.toString()) builder.appendQueryParameter("releasedType", state.toString())
} }
@ -296,18 +321,22 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) : ParsedHttpSour
} }
} }
class TypeFilterGroup : UriGroup<TypeFilter>("Type", listOf( class TypeFilterGroup : UriGroup<TypeFilter>(
"Type",
listOf(
TypeFilter("Japanese Manga", 0), TypeFilter("Japanese Manga", 0),
TypeFilter("Korean Manhwa", 1), TypeFilter("Korean Manhwa", 1),
TypeFilter("Chinese Manhua", 2), TypeFilter("Chinese Manhua", 2),
TypeFilter("Comic", 3), TypeFilter("Comic", 3),
TypeFilter("Doujinshi", 4) TypeFilter("Doujinshi", 4)
)) )
)
class TypeFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter { class TypeFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
if (state) if (state) {
builder.appendQueryParameter("type", id.toString()) builder.appendQueryParameter("type", id.toString())
}
} }
} }

View File

@ -41,9 +41,10 @@ import rx.schedulers.Schedulers
typealias SiteMap = NakedTrie<Unit> typealias SiteMap = NakedTrie<Unit>
class EightMuses : HttpSource(), class EightMuses :
LewdSource<EightMusesSearchMetadata, Document>, HttpSource(),
UrlImportableSource { LewdSource<EightMusesSearchMetadata, Document>,
UrlImportableSource {
override val id = EIGHTMUSES_SOURCE_ID override val id = EIGHTMUSES_SOURCE_ID
/** /**
@ -74,10 +75,10 @@ class EightMuses : HttpSource(),
private suspend fun obtainSiteMap() = siteMapCache.obtain { private suspend fun obtainSiteMap() = siteMapCache.obtain {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val result = client.newCall(eightMusesGet("$baseUrl/sitemap/1.xml")) val result = client.newCall(eightMusesGet("$baseUrl/sitemap/1.xml"))
.asObservableSuccess() .asObservableSuccess()
.toSingle() .toSingle()
.await(Schedulers.io()) .await(Schedulers.io())
.body!!.string() .body!!.string()
val parsed = Jsoup.parse(result) val parsed = Jsoup.parse(result)
@ -93,10 +94,10 @@ class EightMuses : HttpSource(),
override fun headersBuilder(): Headers.Builder { override fun headersBuilder(): Headers.Builder {
return Headers.Builder() return Headers.Builder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;") .add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;")
.add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8") .add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
.add("Referer", "https://www.8muses.com") .add("Referer", "https://www.8muses.com")
.add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36") .add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36")
} }
private fun eightMusesGet(url: String): Request { private fun eightMusesGet(url: String): Request {
@ -129,11 +130,11 @@ class EightMuses : HttpSource(),
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = if (!query.isBlank()) { val urlBuilder = if (!query.isBlank()) {
"$baseUrl/search".toHttpUrlOrNull()!! "$baseUrl/search".toHttpUrlOrNull()!!
.newBuilder() .newBuilder()
.addQueryParameter("q", query) .addQueryParameter("q", query)
} else { } else {
"$baseUrl/comics".toHttpUrlOrNull()!! "$baseUrl/comics".toHttpUrlOrNull()!!
.newBuilder() .newBuilder()
} }
urlBuilder.addQueryParameter("page", page.toString()) urlBuilder.addQueryParameter("page", page.toString())
@ -182,12 +183,14 @@ class EightMuses : HttpSource(),
private fun fetchListing(request: Request, dig: Boolean): Observable<MangasPage> { private fun fetchListing(request: Request, dig: Boolean): Observable<MangasPage> {
return client.newCall(request) return client.newCall(request)
.asObservableSuccess() .asObservableSuccess()
.flatMapSingle { response -> .flatMapSingle { response ->
RxJavaInterop.toV1Single(GlobalScope.async(Dispatchers.IO) { RxJavaInterop.toV1Single(
GlobalScope.async(Dispatchers.IO) {
parseResultsPage(response, dig) parseResultsPage(response, dig)
}.asSingle(GlobalScope.coroutineContext)) }.asSingle(GlobalScope.coroutineContext)
} )
}
} }
private suspend fun parseResultsPage(response: Response, dig: Boolean): MangasPage { private suspend fun parseResultsPage(response: Response, dig: Boolean): MangasPage {
@ -197,32 +200,32 @@ class EightMuses : HttpSource(),
val onLastPage = doc.selectFirst(".current:nth-last-child(2)") != null val onLastPage = doc.selectFirst(".current:nth-last-child(2)") != null
return MangasPage( return MangasPage(
if (dig) { if (dig) {
contents.albums.flatMap { contents.albums.flatMap {
val href = it.attr("href") val href = it.attr("href")
val splitHref = href.split('/') val splitHref = href.split('/')
obtainSiteMap().subMap(href).filter { obtainSiteMap().subMap(href).filter {
it.key.split('/').size - splitHref.size == 1 it.key.split('/').size - splitHref.size == 1
}.map { (key, _) -> }.map { (key, _) ->
SManga.create().apply {
url = key
title = key.substringAfterLast('/').replace('-', ' ')
}
}
}
} else {
contents.albums.map {
SManga.create().apply { SManga.create().apply {
url = it.attr("href") url = key
title = it.select(".title-text").text() title = key.substringAfterLast('/').replace('-', ' ')
thumbnail_url = baseUrl + it.select(".lazyload").attr("data-src")
} }
} }
}, }
!onLastPage } else {
contents.albums.map {
SManga.create().apply {
url = it.attr("href")
title = it.select(".title-text").text()
thumbnail_url = baseUrl + it.select(".lazyload").attr("data-src")
}
}
},
!onLastPage
) )
} }
@ -243,10 +246,10 @@ class EightMuses : HttpSource(),
*/ */
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga)) parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
} }
} }
/** /**
@ -259,9 +262,11 @@ class EightMuses : HttpSource(),
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return RxJavaInterop.toV1Single(GlobalScope.async(Dispatchers.IO) { return RxJavaInterop.toV1Single(
fetchAndParseChapterList("", manga.url) GlobalScope.async(Dispatchers.IO) {
}.asSingle(GlobalScope.coroutineContext)).toObservable() fetchAndParseChapterList("", manga.url)
}.asSingle(GlobalScope.coroutineContext)
).toObservable()
} }
private suspend fun fetchAndParseChapterList(prefix: String, url: String): List<SChapter> { private suspend fun fetchAndParseChapterList(prefix: String, url: String): List<SChapter> {
@ -309,9 +314,9 @@ class EightMuses : HttpSource(),
val contents = parseSelf(response.asJsoup()) val contents = parseSelf(response.asJsoup())
return contents.images.mapIndexed { index, element -> return contents.images.mapIndexed { index, element ->
Page( Page(
index, index,
element.attr("href"), element.attr("href"),
"$baseUrl/image/fl" + element.select(".lazyload").attr("data-src").substring(9) "$baseUrl/image/fl" + element.select(".lazyload").attr("data-src").substring(9)
) )
} }
} }
@ -325,30 +330,30 @@ class EightMuses : HttpSource(),
title = breadcrumbs.selectFirst("li:nth-last-child(1) > a").text() title = breadcrumbs.selectFirst("li:nth-last-child(1) > a").text()
thumbnailUrl = parseSelf(input).let { it.albums + it.images }.firstOrNull() thumbnailUrl = parseSelf(input).let { it.albums + it.images }.firstOrNull()
?.selectFirst(".lazyload") ?.selectFirst(".lazyload")
?.attr("data-src")?.let { ?.attr("data-src")?.let {
baseUrl + it baseUrl + it
} }
tags.clear() tags.clear()
tags += RaisedTag( tags += RaisedTag(
EightMusesSearchMetadata.ARTIST_NAMESPACE, EightMusesSearchMetadata.ARTIST_NAMESPACE,
breadcrumbs.selectFirst("li:nth-child(2) > a").text(), breadcrumbs.selectFirst("li:nth-child(2) > a").text(),
EightMusesSearchMetadata.TAG_TYPE_DEFAULT EightMusesSearchMetadata.TAG_TYPE_DEFAULT
) )
tags += input.select(".album-tags a").map { tags += input.select(".album-tags a").map {
RaisedTag( RaisedTag(
EightMusesSearchMetadata.TAGS_NAMESPACE, EightMusesSearchMetadata.TAGS_NAMESPACE,
it.text(), it.text(),
EightMusesSearchMetadata.TAG_TYPE_DEFAULT EightMusesSearchMetadata.TAG_TYPE_DEFAULT
) )
} }
} }
} }
class SortFilter : Filter.Select<String>( class SortFilter : Filter.Select<String>(
"Sort", "Sort",
SORT_OPTIONS.map { it.second }.toTypedArray() SORT_OPTIONS.map { it.second }.toTypedArray()
) { ) {
fun addToUri(url: HttpUrl.Builder) { fun addToUri(url: HttpUrl.Builder) {
url.addQueryParameter("sort", SORT_OPTIONS[state].first) url.addQueryParameter("sort", SORT_OPTIONS[state].first)
@ -357,16 +362,16 @@ class EightMuses : HttpSource(),
companion object { companion object {
// <Internal, Display> // <Internal, Display>
private val SORT_OPTIONS = listOf( private val SORT_OPTIONS = listOf(
"" to "Views", "" to "Views",
"like" to "Likes", "like" to "Likes",
"date" to "Date", "date" to "Date",
"az" to "A-Z" "az" to "A-Z"
) )
} }
} }
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
SortFilter() SortFilter()
) )
/** /**
@ -379,8 +384,8 @@ class EightMuses : HttpSource(),
} }
override val matchingHosts = listOf( override val matchingHosts = listOf(
"www.8muses.com", "www.8muses.com",
"8muses.com" "8muses.com"
) )
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {

View File

@ -19,8 +19,10 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate), class HentaiCafe(delegate: HttpSource) :
LewdSource<HentaiCafeSearchMetadata, Document>, UrlImportableSource { DelegatedHttpSource(delegate),
LewdSource<HentaiCafeSearchMetadata, Document>,
UrlImportableSource {
/** /**
* An ISO 639-1 compliant language code (two letters in lower case). * An ISO 639-1 compliant language code (two letters in lower case).
*/ */
@ -32,18 +34,22 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply { parseToManga(manga, it.asJsoup()).andThen(
initialized = true Observable.just(
})) manga.apply {
} initialized = true
}
)
)
}
} }
/** /**
@ -57,8 +63,8 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
thumbnailUrl = contentElement.child(0).child(0).attr("src") thumbnailUrl = contentElement.child(0).child(0).attr("src")
fun filterableTagsOfType(type: String) = contentElement.select("a") fun filterableTagsOfType(type: String) = contentElement.select("a")
.filter { "$baseUrl/hc.fyi/$type/" in it.attr("href") } .filter { "$baseUrl/hc.fyi/$type/" in it.attr("href") }
.map { it.text() } .map { it.text() }
tags.clear() tags.clear()
tags += filterableTagsOfType("tag").map { tags += filterableTagsOfType("tag").map {
@ -78,29 +84,30 @@ class HentaiCafe(delegate: HttpSource) : DelegatedHttpSource(delegate),
override fun fetchChapterList(manga: SManga) = getOrLoadMetadata(manga.id) { override fun fetchChapterList(manga: SManga) = getOrLoadMetadata(manga.id) {
client.newCall(mangaDetailsRequest(manga)) client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { it.asJsoup() } .map { it.asJsoup() }
.toSingle() .toSingle()
}.map { }.map {
listOf( listOf(
SChapter.create().apply { SChapter.create().apply {
setUrlWithoutDomain("/manga/read/${it.readerId}/en/0/1/") setUrlWithoutDomain("/manga/read/${it.readerId}/en/0/1/")
name = "Chapter" name = "Chapter"
chapter_number = 0.0f chapter_number = 0.0f
} }
) )
}.toObservable() }.toObservable()
override val matchingHosts = listOf( override val matchingHosts = listOf(
"hentai.cafe" "hentai.cafe"
) )
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
return if (lcFirstPathSegment == "manga") return if (lcFirstPathSegment == "manga") {
"https://hentai.cafe/${uri.pathSegments[2]}" "https://hentai.cafe/${uri.pathSegments[2]}"
else } else {
"https://hentai.cafe/$lcFirstPathSegment" "https://hentai.cafe/$lcFirstPathSegment"
}
} }
} }

View File

@ -18,8 +18,10 @@ import exh.util.urlImportFetchSearchManga
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate), class Pururin(delegate: HttpSource) :
LewdSource<PururinSearchMetadata, Document>, UrlImportableSource { DelegatedHttpSource(delegate),
LewdSource<PururinSearchMetadata, Document>,
UrlImportableSource {
/** /**
* An ISO 639-1 compliant language code (two letters in lower case). * An ISO 639-1 compliant language code (two letters in lower case).
*/ */
@ -43,11 +45,11 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
parseToManga(manga, it.asJsoup()) parseToManga(manga, it.asJsoup())
.andThen(Observable.just(manga)) .andThen(Observable.just(manga))
} }
} }
override fun parseIntoMetadata(metadata: PururinSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: PururinSearchMetadata, input: Document) {
@ -87,9 +89,9 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
value.select("a").forEach { link -> value.select("a").forEach { link ->
val searchUrl = Uri.parse(link.attr("href")) val searchUrl = Uri.parse(link.attr("href"))
tags += RaisedTag( tags += RaisedTag(
searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2], searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2],
searchUrl.lastPathSegment!!.substringBefore("."), searchUrl.lastPathSegment!!.substringBefore("."),
PururinSearchMetadata.TAG_TYPE_DEFAULT PururinSearchMetadata.TAG_TYPE_DEFAULT
) )
} }
} }
@ -99,8 +101,8 @@ class Pururin(delegate: HttpSource) : DelegatedHttpSource(delegate),
} }
override val matchingHosts = listOf( override val matchingHosts = listOf(
"pururin.io", "pururin.io",
"www.pururin.io" "www.pururin.io"
) )
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {

View File

@ -19,30 +19,33 @@ import java.util.Locale
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate), class Tsumino(delegate: HttpSource) :
LewdSource<TsuminoSearchMetadata, Document>, UrlImportableSource { DelegatedHttpSource(delegate),
LewdSource<TsuminoSearchMetadata, Document>,
UrlImportableSource {
override val metaClass = TsuminoSearchMetadata::class override val metaClass = TsuminoSearchMetadata::class
override val lang = "en" override val lang = "en"
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") {
return null return null
}
return "https://tsumino.com/Book/Info/${uri.lastPathSegment}" return "https://tsumino.com/Book/Info/${uri.lastPathSegment}"
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga)) parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
} }
} }
override fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) {
@ -106,16 +109,18 @@ class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
character = newCharacter character = newCharacter
input.getElementById("Tag")?.children()?.let { input.getElementById("Tag")?.children()?.let {
tags.addAll(it.map { tags.addAll(
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT) it.map {
}) RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT)
}
)
} }
} }
} }
override val matchingHosts = listOf( override val matchingHosts = listOf(
"www.tsumino.com", "www.tsumino.com",
"tsumino.com" "tsumino.com"
) )
companion object { companion object {

View File

@ -131,10 +131,14 @@ class SourceController :
// Open the catalogue view. // Open the catalogue view.
openCatalogue(source, BrowseSourceController(source)) openCatalogue(source, BrowseSourceController(source))
} }
Mode.SMART_SEARCH -> router.pushController(SmartSearchController(Bundle().apply { Mode.SMART_SEARCH -> router.pushController(
putLong(SmartSearchController.ARG_SOURCE_ID, source.id) SmartSearchController(
putParcelable(SmartSearchController.ARG_SMART_SEARCH_CONFIG, smartSearchConfig) Bundle().apply {
}).withFadeTransaction()) putLong(SmartSearchController.ARG_SOURCE_ID, source.id)
putParcelable(SmartSearchController.ARG_SMART_SEARCH_CONFIG, smartSearchConfig)
}
).withFadeTransaction()
)
} }
return false return false
} }

View File

@ -413,9 +413,9 @@ open class BrowseSourcePresenter(
} }
val newSerialized = searches.map { val newSerialized = searches.map {
"${source.id}:" + jsonObject( "${source.id}:" + jsonObject(
"name" to it.name, "name" to it.name,
"query" to it.query, "query" to it.query,
"filters" to filterSerializer.serialize(it.filterList) "filters" to filterSerializer.serialize(it.filterList)
).toString() ).toString()
} }
prefs.eh_savedSearches().set((otherSerialized + newSerialized).toSet()) prefs.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
@ -430,9 +430,11 @@ open class BrowseSourcePresenter(
val content = JsonParser.parseString(it.substringAfter(':')).obj val content = JsonParser.parseString(it.substringAfter(':')).obj
val originalFilters = source.getFilterList() val originalFilters = source.getFilterList()
filterSerializer.deserialize(originalFilters, content["filters"].array) filterSerializer.deserialize(originalFilters, content["filters"].array)
EXHSavedSearch(content["name"].string, EXHSavedSearch(
content["query"].string, content["name"].string,
originalFilters) content["query"].string,
originalFilters
)
} catch (t: RuntimeException) { } catch (t: RuntimeException) {
// Load failed // Load failed
Timber.e(t, "Failed to load saved search!") Timber.e(t, "Failed to load saved search!")

View File

@ -393,7 +393,6 @@ class LibraryPresenter(
manga: Manga, manga: Manga,
replace: Boolean replace: Boolean
) { ) {
val flags = preferences.migrateFlags().get() val flags = preferences.migrateFlags().get()
val migrateChapters = MigrationFlags.hasChapters(flags) val migrateChapters = MigrationFlags.hasChapters(flags)
val migrateCategories = MigrationFlags.hasCategories(flags) val migrateCategories = MigrationFlags.hasCategories(flags)

View File

@ -181,29 +181,34 @@ class MangaInfoPresenter(
suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga { suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga {
val originalManga = db.getManga(originalMangaId).await() val originalManga = db.getManga(originalMangaId).await()
?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId") ?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
val toInsert = if (originalManga.source == MERGED_SOURCE_ID) { val toInsert = if (originalManga.source == MERGED_SOURCE_ID) {
originalManga.apply { originalManga.apply {
val originalChildren = MergedSource.MangaConfig.readFromUrl(gson, url).children val originalChildren = MergedSource.MangaConfig.readFromUrl(gson, url).children
if (originalChildren.any { it.source == manga.source && it.url == manga.url }) if (originalChildren.any { it.source == manga.source && it.url == manga.url }) {
throw IllegalArgumentException("This manga is already merged with the current manga!") throw IllegalArgumentException("This manga is already merged with the current manga!")
}
url = MergedSource.MangaConfig(originalChildren + MergedSource.MangaSource( url = MergedSource.MangaConfig(
originalChildren + MergedSource.MangaSource(
manga.source, manga.source,
manga.url manga.url
)).writeAsUrl(gson) )
).writeAsUrl(gson)
} }
} else { } else {
val newMangaConfig = MergedSource.MangaConfig(listOf( val newMangaConfig = MergedSource.MangaConfig(
listOf(
MergedSource.MangaSource( MergedSource.MangaSource(
originalManga.source, originalManga.source,
originalManga.url originalManga.url
), ),
MergedSource.MangaSource( MergedSource.MangaSource(
manga.source, manga.source,
manga.url manga.url
) )
)) )
)
Manga.create(newMangaConfig.writeAsUrl(gson), originalManga.title, MERGED_SOURCE_ID).apply { Manga.create(newMangaConfig.writeAsUrl(gson), originalManga.title, MERGED_SOURCE_ID).apply {
copyFrom(originalManga) copyFrom(originalManga)
favorite = true favorite = true

View File

@ -23,17 +23,22 @@ class MigrationMangaDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val confirmRes = if (copy) R.plurals.copy_manga else R.plurals.migrate_manga val confirmRes = if (copy) R.plurals.copy_manga else R.plurals.migrate_manga
val confirmString = applicationContext?.resources?.getQuantityString(confirmRes, mangaSet, val confirmString = applicationContext?.resources?.getQuantityString(
mangaSet, ( confirmRes, mangaSet,
mangaSet,
(
if (mangaSkipped > 0) " " + applicationContext?.getString(R.string.skipping_, mangaSkipped) if (mangaSkipped > 0) " " + applicationContext?.getString(R.string.skipping_, mangaSkipped)
else "")) ?: "" else ""
)
) ?: ""
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.message(text = confirmString) .message(text = confirmString)
.positiveButton(if (copy) R.string.copy else R.string.migrate) { .positiveButton(if (copy) R.string.copy else R.string.migrate) {
if (copy) if (copy) {
(targetController as? MigrationListController)?.copyMangas() (targetController as? MigrationListController)?.copyMangas()
else } else {
(targetController as? MigrationListController)?.migrateMangas() (targetController as? MigrationListController)?.migrateMangas()
}
} }
.negativeButton(android.R.string.no) .negativeButton(android.R.string.no)
} }

View File

@ -31,10 +31,12 @@ class MigrationBottomSheetDialog(
activity: Activity, activity: Activity,
theme: Int, theme: Int,
private val listener: private val listener:
StartMigrationListener StartMigrationListener
) : ) :
BottomSheetDialog(activity, BottomSheetDialog(
theme) { activity,
theme
) {
/** /**
* Preferences helper. * Preferences helper.
*/ */
@ -47,8 +49,9 @@ class MigrationBottomSheetDialog(
// scroll.addView(view) // scroll.addView(view)
setContentView(view) setContentView(view)
if (activity.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) if (activity.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) {
sourceGroup.orientation = LinearLayout.HORIZONTAL sourceGroup.orientation = LinearLayout.HORIZONTAL
}
window?.setBackgroundDrawable(null) window?.setBackgroundDrawable(null)
} }
@ -63,8 +66,10 @@ class MigrationBottomSheetDialog(
fab.setOnClickListener { fab.setOnClickListener {
preferences.skipPreMigration().set(skip_step.isChecked) preferences.skipPreMigration().set(skip_step.isChecked)
listener.startMigration( listener.startMigration(
if (use_smart_search.isChecked && extra_search_param_text.text.isNotBlank()) if (use_smart_search.isChecked && extra_search_param_text.text.isNotBlank()) {
extra_search_param_text.text.toString() else null) extra_search_param_text.text.toString()
} else null
)
dismiss() dismiss()
} }
} }
@ -96,9 +101,12 @@ class MigrationBottomSheetDialog(
skip_step.isChecked = preferences.skipPreMigration().get() skip_step.isChecked = preferences.skipPreMigration().get()
skip_step.setOnCheckedChangeListener { _, isChecked -> skip_step.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) if (isChecked) {
(listener as? Controller)?.activity?.toast(R.string.pre_migration_skip_toast, (listener as? Controller)?.activity?.toast(
Toast.LENGTH_LONG) R.string.pre_migration_skip_toast,
Toast.LENGTH_LONG
)
}
} }
} }

View File

@ -9,16 +9,21 @@ class MigrationSourceAdapter(
var items: List<MigrationSourceItem>, var items: List<MigrationSourceItem>,
val controllerPre: PreMigrationController val controllerPre: PreMigrationController
) : FlexibleAdapter<MigrationSourceItem>( ) : FlexibleAdapter<MigrationSourceItem>(
items, items,
controllerPre, controllerPre,
true true
) { ) {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putParcelableArrayList(SELECTED_SOURCES_KEY, ArrayList(currentItems.map { outState.putParcelableArrayList(
it.asParcelable() SELECTED_SOURCES_KEY,
})) ArrayList(
currentItems.map {
it.asParcelable()
}
)
)
} }
override fun onRestoreInstanceState(savedInstanceState: Bundle) { override fun onRestoreInstanceState(savedInstanceState: Bundle) {

View File

@ -66,8 +66,8 @@ class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean) :
val source = sourceManager.get(si.sourceId) as? HttpSource ?: return null val source = sourceManager.get(si.sourceId) as? HttpSource ?: return null
return MigrationSourceItem( return MigrationSourceItem(
source, source,
si.sourceEnabled si.sourceEnabled
) )
} }
} }

View File

@ -27,8 +27,11 @@ import exh.util.updateLayoutParams
import exh.util.updatePaddingRelative import exh.util.updatePaddingRelative
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrationControllerBinding>(bundle), FlexibleAdapter class PreMigrationController(bundle: Bundle? = null) :
.OnItemClickListener, StartMigrationListener { BaseController<PreMigrationControllerBinding>(bundle),
FlexibleAdapter
.OnItemClickListener,
StartMigrationListener {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
@ -69,8 +72,10 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrati
bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom
} }
// offset the recycler by the fab's inset + some inset on top // offset the recycler by the fab's inset + some inset on top
v.updatePaddingRelative(bottom = padding.bottom + (binding.fab.marginBottom) + v.updatePaddingRelative(
fabBaseMarginBottom + (binding.fab.height)) bottom = padding.bottom + (binding.fab.marginBottom) +
fabBaseMarginBottom + (binding.fab.height)
)
} }
binding.fab.setOnClickListener { binding.fab.setOnClickListener {
@ -101,7 +106,8 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrati
config.toList(), config.toList(),
extraSearchParams = extraParam extraSearchParams = extraParam
) )
).withFadeTransaction().tag(MigrationListController.TAG)) ).withFadeTransaction().tag(MigrationListController.TAG)
)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -136,10 +142,13 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrati
.filter { it.lang in languages } .filter { it.lang in languages }
.sortedBy { "(${it.lang}) ${it.name}" } .sortedBy { "(${it.lang}) ${it.name}" }
sources = sources =
sources.filter { isEnabled(it.id.toString()) }.sortedBy { sourcesSaved.indexOf(it.id sources.filter { isEnabled(it.id.toString()) }.sortedBy {
.toString()) sourcesSaved.indexOf(
} + it.id
sources.filterNot { isEnabled(it.id.toString()) } .toString()
)
} +
sources.filterNot { isEnabled(it.id.toString()) }
return sources return sources
} }
@ -167,9 +176,11 @@ class PreMigrationController(bundle: Bundle? = null) : BaseController<PreMigrati
} }
fun create(mangaIds: List<Long>): PreMigrationController { fun create(mangaIds: List<Long>): PreMigrationController {
return PreMigrationController(Bundle().apply { return PreMigrationController(
putLongArray(MANGA_IDS_EXTRA, mangaIds.toLongArray()) Bundle().apply {
}) putLongArray(MANGA_IDS_EXTRA, mangaIds.toLongArray())
}
)
} }
} }
} }

View File

@ -56,8 +56,10 @@ import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class MigrationListController(bundle: Bundle? = null) : BaseController<MigrationListControllerBinding>(bundle), class MigrationListController(bundle: Bundle? = null) :
MigrationProcessAdapter.MigrationProcessInterface, CoroutineScope { BaseController<MigrationListControllerBinding>(bundle),
MigrationProcessAdapter.MigrationProcessInterface,
CoroutineScope {
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -93,7 +95,6 @@ class MigrationListController(bundle: Bundle? = null) : BaseController<Migration
} }
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
view.applyWindowInsetsForController() view.applyWindowInsetsForController()
setTitle() setTitle()
@ -217,7 +218,8 @@ class MigrationListController(bundle: Bundle? = null) : BaseController<Migration
val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id) val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
val chapters = try { val chapters = try {
source.fetchChapterList(localManga) source.fetchChapterList(localManga)
.toSingle().await(Schedulers.io()) } catch (e: java.lang.Exception) { .toSingle().await(Schedulers.io())
} catch (e: java.lang.Exception) {
Timber.e(e) Timber.e(e)
emptyList<SChapter>() emptyList<SChapter>()
} ?: emptyList() } ?: emptyList()
@ -313,7 +315,6 @@ class MigrationListController(bundle: Bundle? = null) : BaseController<Migration
} }
override fun onMenuItemClick(position: Int, item: MenuItem) { override fun onMenuItemClick(position: Int, item: MenuItem) {
when (item.itemId) { when (item.itemId) {
R.id.action_search_manually -> { R.id.action_search_manually -> {
launchUI { launchUI {
@ -488,9 +489,11 @@ class MigrationListController(bundle: Bundle? = null) : BaseController<Migration
const val TAG = "migration_list" const val TAG = "migration_list"
fun create(config: MigrationProcedureConfig): MigrationListController { fun create(config: MigrationProcedureConfig): MigrationListController {
return MigrationListController(Bundle().apply { return MigrationListController(
putParcelable(CONFIG_EXTRA, config) Bundle().apply {
}) putParcelable(CONFIG_EXTRA, config)
}
)
} }
} }
} }

View File

@ -42,8 +42,12 @@ class MigrationProcessAdapter(
if (allMangasDone()) menuItemListener.enableButtons() if (allMangasDone()) menuItemListener.enableButtons()
} }
fun allMangasDone() = (items.all { it.manga.migrationStatus != MigrationStatus fun allMangasDone() = (
.RUNNUNG } && items.any { it.manga.migrationStatus == MigrationStatus.MANGA_FOUND }) items.all {
it.manga.migrationStatus != MigrationStatus
.RUNNUNG
} && items.any { it.manga.migrationStatus == MigrationStatus.MANGA_FOUND }
)
fun mangasSkipped() = (items.count { it.manga.migrationStatus == MigrationStatus.MANGA_NOT_FOUND }) fun mangasSkipped() = (items.count { it.manga.migrationStatus == MigrationStatus.MANGA_NOT_FOUND })
@ -59,7 +63,8 @@ class MigrationProcessAdapter(
migrateMangaInternal( migrateMangaInternal(
manga.manga() ?: return@forEach, manga.manga() ?: return@forEach,
toMangaObj, toMangaObj,
!copy) !copy
)
} }
} }
} }

View File

@ -48,10 +48,18 @@ class MigrationProcessHolder(
val manga = item.manga.manga() val manga = item.manga.manga()
val source = item.manga.mangaSource() val source = item.manga.mangaSource()
migration_menu.setVectorCompat(R.drawable.ic_more_vert_24dp, view.context migration_menu.setVectorCompat(
.getResourceColor(R.attr.colorOnPrimary)) R.drawable.ic_more_vert_24dp,
skip_manga.setVectorCompat(R.drawable.ic_close_24dp, view.context.getResourceColor(R view.context
.attr.colorOnPrimary)) .getResourceColor(R.attr.colorOnPrimary)
)
skip_manga.setVectorCompat(
R.drawable.ic_close_24dp,
view.context.getResourceColor(
R
.attr.colorOnPrimary
)
)
migration_menu.invisible() migration_menu.invisible()
skip_manga.visible() skip_manga.visible()
migration_manga_card_to.resetManga() migration_manga_card_to.resetManga()
@ -87,7 +95,8 @@ class MigrationProcessHolder(
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (item.manga.mangaId != this@MigrationProcessHolder.item?.manga?.mangaId || if (item.manga.mangaId != this@MigrationProcessHolder.item?.manga?.mangaId ||
item.manga.migrationStatus == MigrationStatus.RUNNUNG) { item.manga.migrationStatus == MigrationStatus.RUNNUNG
) {
return@withContext return@withContext
} }
if (searchResult != null && resultSource != null) { if (searchResult != null && resultSource != null) {
@ -152,11 +161,15 @@ class MigrationProcessHolder(
val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
if (latestChapter > 0f) { if (latestChapter > 0f) {
manga_last_chapter_label.text = context.getString(R.string.latest_, manga_last_chapter_label.text = context.getString(
DecimalFormat("#.#").format(latestChapter)) R.string.latest_,
DecimalFormat("#.#").format(latestChapter)
)
} else { } else {
manga_last_chapter_label.text = context.getString(R.string.latest_, manga_last_chapter_label.text = context.getString(
context.getString(R.string.unknown)) R.string.latest_,
context.getString(R.string.unknown)
)
} }
} }

View File

@ -63,13 +63,13 @@ class SettingsEhController : SettingsController() {
private fun Preference<*>.reconfigure(): Boolean { private fun Preference<*>.reconfigure(): Boolean {
// Listen for change commit // Listen for change commit
asObservable() asObservable()
.skip(1) // Skip first as it is emitted immediately .skip(1) // Skip first as it is emitted immediately
.take(1) // Only listen for first commit .take(1) // Only listen for first commit
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { .subscribeUntilDestroy {
// Only listen for first change commit // Only listen for first change commit
WarnConfigureDialogController.uploadSettings(router) WarnConfigureDialogController.uploadSettings(router)
} }
// Always return true to save changes // Always return true to save changes
return true return true
@ -85,12 +85,12 @@ class SettingsEhController : SettingsController() {
isPersistent = false isPersistent = false
defaultValue = false defaultValue = false
preferences.enableExhentai() preferences.enableExhentai()
.asObservable() .asObservable()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { .subscribeUntilDestroy {
isChecked = it isChecked = it
} }
onChange { newVal -> onChange { newVal ->
newVal as Boolean newVal as Boolean
@ -98,9 +98,11 @@ class SettingsEhController : SettingsController() {
preferences.enableExhentai().set(false) preferences.enableExhentai().set(false)
true true
} else { } else {
router.pushController(RouterTransaction.with(LoginController()) router.pushController(
RouterTransaction.with(LoginController())
.pushChangeHandler(FadeChangeHandler()) .pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())) .popChangeHandler(FadeChangeHandler())
)
false false
} }
} }
@ -148,20 +150,20 @@ class SettingsEhController : SettingsController() {
summary = "The quality of the downloaded images" summary = "The quality of the downloaded images"
title = "Image quality" title = "Image quality"
entries = arrayOf( entries = arrayOf(
"Auto", "Auto",
"2400x", "2400x",
"1600x", "1600x",
"1280x", "1280x",
"980x", "980x",
"780x" "780x"
) )
entryValues = arrayOf( entryValues = arrayOf(
"auto", "auto",
"ovrs_2400", "ovrs_2400",
"ovrs_1600", "ovrs_1600",
"high", "high",
"med", "med",
"low" "low"
) )
onChange { preferences.imageQuality().reconfigure() } onChange { preferences.imageQuality().reconfigure() }
@ -202,21 +204,21 @@ class SettingsEhController : SettingsController() {
onClick { onClick {
activity?.let { activity -> activity?.let { activity ->
MaterialDialog(activity) MaterialDialog(activity)
.title(R.string.eh_force_sync_reset_title) .title(R.string.eh_force_sync_reset_title)
.message(R.string.eh_force_sync_reset_message) .message(R.string.eh_force_sync_reset_message)
.positiveButton(android.R.string.yes) { .positiveButton(android.R.string.yes) {
LocalFavoritesStorage().apply { LocalFavoritesStorage().apply {
getRealm().use { getRealm().use {
it.trans { it.trans {
clearSnapshots(it) clearSnapshots(it)
}
} }
} }
activity.toast("Sync state reset", Toast.LENGTH_LONG)
} }
.negativeButton(android.R.string.no) activity.toast("Sync state reset", Toast.LENGTH_LONG)
.cancelable(false) }
.show() .negativeButton(android.R.string.no)
.cancelable(false)
.show()
} }
} }
} }
@ -311,18 +313,18 @@ class SettingsEhController : SettingsController() {
} }
""" """
$statsText $statsText
Galleries that were checked in the last: Galleries that were checked in the last:
- hour: ${metaInRelativeDuration(1.hours)} - hour: ${metaInRelativeDuration(1.hours)}
- 6 hours: ${metaInRelativeDuration(6.hours)} - 6 hours: ${metaInRelativeDuration(6.hours)}
- 12 hours: ${metaInRelativeDuration(12.hours)} - 12 hours: ${metaInRelativeDuration(12.hours)}
- day: ${metaInRelativeDuration(1.days)} - day: ${metaInRelativeDuration(1.days)}
- 2 days: ${metaInRelativeDuration(2.days)} - 2 days: ${metaInRelativeDuration(2.days)}
- week: ${metaInRelativeDuration(7.days)} - week: ${metaInRelativeDuration(7.days)}
- month: ${metaInRelativeDuration(30.days)} - month: ${metaInRelativeDuration(30.days)}
- year: ${metaInRelativeDuration(365.days)} - year: ${metaInRelativeDuration(365.days)}
""".trimIndent() """.trimIndent()
} finally { } finally {
progress.dismiss() progress.dismiss()
} }

View File

@ -75,10 +75,12 @@ class SettingsLibraryController : SettingsController() {
intListPreference { intListPreference {
key = Keys.eh_library_rounded_corners key = Keys.eh_library_rounded_corners
title = "Rounded Corner Radius" title = "Rounded Corner Radius"
entriesRes = arrayOf(R.string.eh_rounded_corner_0, R.string.eh_rounded_corner_1, entriesRes = arrayOf(
R.string.eh_rounded_corner_0, R.string.eh_rounded_corner_1,
R.string.eh_rounded_corner_2, R.string.eh_rounded_corner_3, R.string.eh_rounded_corner_4, R.string.eh_rounded_corner_2, R.string.eh_rounded_corner_3, R.string.eh_rounded_corner_4,
R.string.eh_rounded_corner_5, R.string.eh_rounded_corner_6, R.string.eh_rounded_corner_7, R.string.eh_rounded_corner_5, R.string.eh_rounded_corner_6, R.string.eh_rounded_corner_7,
R.string.eh_rounded_corner_8, R.string.eh_rounded_corner_9, R.string.eh_rounded_corner_10) R.string.eh_rounded_corner_8, R.string.eh_rounded_corner_9, R.string.eh_rounded_corner_10
)
entryValues = arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10") entryValues = arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")
defaultValue = "4" defaultValue = "4"
summaryRes = R.string.eh_rounded_corners_desc summaryRes = R.string.eh_rounded_corners_desc
@ -211,7 +213,8 @@ class SettingsLibraryController : SettingsController() {
} }
} }
if (preferences.skipPreMigration().get() || preferences.migrationSources() if (preferences.skipPreMigration().get() || preferences.migrationSources()
.getOrDefault().isNotEmpty()) { .getOrDefault().isNotEmpty()
) {
switchPreference { switchPreference {
key = Keys.skipPreMigration key = Keys.skipPreMigration
titleRes = R.string.pref_skip_pre_migration titleRes = R.string.pref_skip_pre_migration

View File

@ -26,19 +26,19 @@ const val HBROWSE_SOURCE_ID = LEWD_SOURCE_SERIES + 12
const val MERGED_SOURCE_ID = LEWD_SOURCE_SERIES + 69 const val MERGED_SOURCE_ID = LEWD_SOURCE_SERIES + 69
private val DELEGATED_LEWD_SOURCES = listOf( private val DELEGATED_LEWD_SOURCES = listOf(
HentaiCafe::class, HentaiCafe::class,
Pururin::class, Pururin::class,
Tsumino::class Tsumino::class
) )
val LIBRARY_UPDATE_EXCLUDED_SOURCES = listOf( val LIBRARY_UPDATE_EXCLUDED_SOURCES = listOf(
EH_SOURCE_ID, EH_SOURCE_ID,
EXH_SOURCE_ID, EXH_SOURCE_ID,
NHENTAI_SOURCE_ID, NHENTAI_SOURCE_ID,
HENTAI_CAFE_SOURCE_ID, HENTAI_CAFE_SOURCE_ID,
TSUMINO_SOURCE_ID, TSUMINO_SOURCE_ID,
HITOMI_SOURCE_ID, HITOMI_SOURCE_ID,
PURURIN_SOURCE_ID PURURIN_SOURCE_ID
) )
private inline fun <reified T> delegatedSourceId(): Long { private inline fun <reified T> delegatedSourceId(): Long {
@ -54,6 +54,6 @@ private val lewdDelegatedSourceIds = SourceManager.DELEGATED_SOURCES.filter {
// This method MUST be fast! // This method MUST be fast!
fun isLewdSource(source: Long) = source in 6900..6999 || fun isLewdSource(source: Long) = source in 6900..6999 ||
lewdDelegatedSourceIds.binarySearch(source) >= 0 lewdDelegatedSourceIds.binarySearch(source) >= 0
fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID fun Source.isEhBasedSource() = id == EH_SOURCE_ID || id == EXH_SOURCE_ID

View File

@ -45,35 +45,41 @@ object EXHMigrations {
if (oldVersion < 1) { if (oldVersion < 1) {
db.inTransaction { db.inTransaction {
// Migrate HentaiCafe source IDs // Migrate HentaiCafe source IDs
db.lowLevel().executeSQL(RawQuery.builder() db.lowLevel().executeSQL(
.query(""" RawQuery.builder()
UPDATE ${MangaTable.TABLE} .query(
SET ${MangaTable.COL_SOURCE} = $HENTAI_CAFE_SOURCE_ID """
WHERE ${MangaTable.COL_SOURCE} = 6908 UPDATE ${MangaTable.TABLE}
""".trimIndent()) SET ${MangaTable.COL_SOURCE} = $HENTAI_CAFE_SOURCE_ID
WHERE ${MangaTable.COL_SOURCE} = 6908
""".trimIndent()
)
.affectsTables(MangaTable.TABLE) .affectsTables(MangaTable.TABLE)
.build()) .build()
)
// Migrate nhentai URLs // Migrate nhentai URLs
val nhentaiManga = db.db.get() val nhentaiManga = db.db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(Query.builder() .withQuery(
.table(MangaTable.TABLE) Query.builder()
.where("${MangaTable.COL_SOURCE} = $NHENTAI_SOURCE_ID") .table(MangaTable.TABLE)
.build()) .where("${MangaTable.COL_SOURCE} = $NHENTAI_SOURCE_ID")
.prepare() .build()
.executeAsBlocking() )
.prepare()
.executeAsBlocking()
nhentaiManga.forEach { nhentaiManga.forEach {
it.url = getUrlWithoutDomain(it.url) it.url = getUrlWithoutDomain(it.url)
} }
db.db.put() db.db.put()
.objects(nhentaiManga) .objects(nhentaiManga)
// Extremely slow without the resolver :/ // Extremely slow without the resolver :/
.withPutResolver(MangaUrlPutResolver()) .withPutResolver(MangaUrlPutResolver())
.prepare() .prepare()
.executeAsBlocking() .executeAsBlocking()
} }
} }
@ -85,14 +91,18 @@ object EXHMigrations {
if (oldVersion < 8405) { if (oldVersion < 8405) {
db.inTransaction { db.inTransaction {
// Migrate HBrowse source IDs // Migrate HBrowse source IDs
db.lowLevel().executeSQL(RawQuery.builder() db.lowLevel().executeSQL(
.query(""" RawQuery.builder()
UPDATE ${MangaTable.TABLE} .query(
SET ${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID """
WHERE ${MangaTable.COL_SOURCE} = 1401584337232758222 UPDATE ${MangaTable.TABLE}
""".trimIndent()) SET ${MangaTable.COL_SOURCE} = $HBROWSE_SOURCE_ID
WHERE ${MangaTable.COL_SOURCE} = 1401584337232758222
""".trimIndent()
)
.affectsTables(MangaTable.TABLE) .affectsTables(MangaTable.TABLE)
.build()) .build()
)
} }
// Cancel old scheduler jobs with old ids // Cancel old scheduler jobs with old ids
@ -101,14 +111,18 @@ object EXHMigrations {
if (oldVersion < 8408) { if (oldVersion < 8408) {
db.inTransaction { db.inTransaction {
// Migrate Tsumino source IDs // Migrate Tsumino source IDs
db.lowLevel().executeSQL(RawQuery.builder() db.lowLevel().executeSQL(
.query(""" RawQuery.builder()
UPDATE ${MangaTable.TABLE} .query(
SET ${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID """
WHERE ${MangaTable.COL_SOURCE} = 6909 UPDATE ${MangaTable.TABLE}
""".trimIndent()) SET ${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID
WHERE ${MangaTable.COL_SOURCE} = 6909
""".trimIndent()
)
.affectsTables(MangaTable.TABLE) .affectsTables(MangaTable.TABLE)
.build()) .build()
)
} }
} }
if (oldVersion < 8409) { if (oldVersion < 8409) {
@ -214,10 +228,12 @@ object EXHMigrations {
return try { return try {
val uri = URI(orig) val uri = URI(orig)
var out = uri.path var out = uri.path
if (uri.query != null) if (uri.query != null) {
out += "?" + uri.query out += "?" + uri.query
if (uri.fragment != null) }
if (uri.fragment != null) {
out += "#" + uri.fragment out += "#" + uri.fragment
}
out out
} catch (e: URISyntaxException) { } catch (e: URISyntaxException) {
orig orig

View File

@ -37,15 +37,15 @@ class GalleryAdder {
} }
} else { } else {
sourceManager.getVisibleCatalogueSources() sourceManager.getVisibleCatalogueSources()
.filterIsInstance<UrlImportableSource>() .filterIsInstance<UrlImportableSource>()
.find { .find {
try { try {
it.matchesUri(uri) it.matchesUri(uri)
} catch (e: Exception) { } catch (e: Exception) {
XLog.e("Source URI match check error!", e) XLog.e("Source URI match check error!", e)
false false
} }
} ?: return GalleryAddEvent.Fail.UnknownType(url) } ?: return GalleryAddEvent.Fail.UnknownType(url)
} }
// Map URL to manga URL // Map URL to manga URL
@ -66,10 +66,10 @@ class GalleryAdder {
// Use manga in DB if possible, otherwise, make a new manga // Use manga in DB if possible, otherwise, make a new manga
val manga = db.getManga(cleanedUrl, source.id).executeAsBlocking() val manga = db.getManga(cleanedUrl, source.id).executeAsBlocking()
?: Manga.create(source.id).apply { ?: Manga.create(source.id).apply {
this.url = cleanedUrl this.url = cleanedUrl
title = realUrl title = realUrl
} }
// Insert created manga if not in DB before fetching details // Insert created manga if not in DB before fetching details
// This allows us to keep the metadata when fetching details // This allows us to keep the metadata when fetching details
@ -111,8 +111,10 @@ class GalleryAdder {
return GalleryAddEvent.Fail.NotFound(url) return GalleryAddEvent.Fail.NotFound(url)
} }
return GalleryAddEvent.Fail.Error(url, return GalleryAddEvent.Fail.Error(
((e.message ?: "Unknown error!") + " (Gallery: $url)").trim()) url,
((e.message ?: "Unknown error!") + " (Gallery: $url)").trim()
)
} }
} }
} }
@ -141,6 +143,6 @@ sealed class GalleryAddEvent {
) : Fail() ) : Fail()
class NotFound(galleryUrl: String) : class NotFound(galleryUrl: String) :
Error(galleryUrl, "Gallery does not exist: $galleryUrl") Error(galleryUrl, "Gallery does not exist: $galleryUrl")
} }
} }

View File

@ -29,7 +29,7 @@ object DebugFunctions {
val sourceManager: SourceManager by injectLazy() val sourceManager: SourceManager by injectLazy()
fun forceUpgradeMigration() { fun forceUpgradeMigration() {
prefs.eh_lastVersionCode().set(0) prefs.eh_lastVersionCode().set(0)
EXHMigrations.upgrade(prefs) EXHMigrations.upgrade(prefs)
} }
@ -38,8 +38,9 @@ object DebugFunctions {
val metadataManga = db.getFavoriteMangaWithMetadata().await() val metadataManga = db.getFavoriteMangaWithMetadata().await()
val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga -> val allManga = metadataManga.asFlow().cancellable().mapNotNull { manga ->
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) {
return@mapNotNull null return@mapNotNull null
}
manga manga
}.toList() }.toList()
@ -56,13 +57,17 @@ object DebugFunctions {
fun addAllMangaInDatabaseToLibrary() { fun addAllMangaInDatabaseToLibrary() {
db.inTransaction { db.inTransaction {
db.lowLevel().executeSQL(RawQuery.builder() db.lowLevel().executeSQL(
.query(""" RawQuery.builder()
UPDATE ${MangaTable.TABLE} .query(
SET ${MangaTable.COL_FAVORITE} = 1 """
""".trimIndent()) UPDATE ${MangaTable.TABLE}
SET ${MangaTable.COL_FAVORITE} = 1
""".trimIndent()
)
.affectsTables(MangaTable.TABLE) .affectsTables(MangaTable.TABLE)
.build()) .build()
)
} }
} }
@ -98,25 +103,29 @@ object DebugFunctions {
fun listScheduledJobs() = app.jobScheduler.allPendingJobs.map { j -> fun listScheduledJobs() = app.jobScheduler.allPendingJobs.map { j ->
""" """
{ {
info: ${j.id}, info: ${j.id},
isPeriod: ${j.isPeriodic}, isPeriod: ${j.isPeriodic},
isPersisted: ${j.isPersisted}, isPersisted: ${j.isPersisted},
intervalMillis: ${j.intervalMillis}, intervalMillis: ${j.intervalMillis},
} }
""".trimIndent() """.trimIndent()
}.joinToString(",\n") }.joinToString(",\n")
fun cancelAllScheduledJobs() = app.jobScheduler.cancelAll() fun cancelAllScheduledJobs() = app.jobScheduler.cancelAll()
private fun convertSources(from: Long, to: Long) { private fun convertSources(from: Long, to: Long) {
db.lowLevel().executeSQL(RawQuery.builder() db.lowLevel().executeSQL(
.query(""" RawQuery.builder()
UPDATE ${MangaTable.TABLE} .query(
SET ${MangaTable.COL_SOURCE} = $to """
WHERE ${MangaTable.COL_SOURCE} = $from UPDATE ${MangaTable.TABLE}
""".trimIndent()) SET ${MangaTable.COL_SOURCE} = $to
WHERE ${MangaTable.COL_SOURCE} = $from
""".trimIndent()
)
.affectsTables(MangaTable.TABLE) .affectsTables(MangaTable.TABLE)
.build()) .build()
)
} }
} }

View File

@ -45,11 +45,11 @@ class SettingsDebugController : SettingsController() {
val result = it.call(DebugFunctions) val result = it.call(DebugFunctions)
view.text = "Function returned result:\n\n$result" view.text = "Function returned result:\n\n$result"
MaterialDialog(context) MaterialDialog(context)
.customView(view = hView, scrollable = true) .customView(view = hView, scrollable = true)
} catch (t: Throwable) { } catch (t: Throwable) {
view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}" view.text = "Function threw exception:\n\n${Log.getStackTraceString(t)}"
MaterialDialog(context) MaterialDialog(context)
.customView(view = hView, scrollable = true) .customView(view = hView, scrollable = true)
}.show() }.show()
} }
} }

View File

@ -12,11 +12,13 @@ class EHentaiThrottleManager(
// Throttle requests if necessary // Throttle requests if necessary
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val timeDiff = now - lastThrottleTime val timeDiff = now - lastThrottleTime
if (timeDiff < throttleTime) if (timeDiff < throttleTime) {
Thread.sleep(throttleTime - timeDiff) Thread.sleep(throttleTime - timeDiff)
}
if (throttleTime < max) if (throttleTime < max) {
throttleTime += inc throttleTime += inc
}
lastThrottleTime = System.currentTimeMillis() lastThrottleTime = System.currentTimeMillis()
} }

View File

@ -17,10 +17,10 @@ data class ChapterChain(val manga: Manga, val chapters: List<Chapter>)
class EHentaiUpdateHelper(context: Context) { class EHentaiUpdateHelper(context: Context) {
val parentLookupTable = val parentLookupTable =
MemAutoFlushingLookupTable( MemAutoFlushingLookupTable(
File(context.filesDir, "exh-plt.maftable"), File(context.filesDir, "exh-plt.maftable"),
GalleryEntry.Serializer() GalleryEntry.Serializer()
) )
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
/** /**
@ -30,22 +30,24 @@ class EHentaiUpdateHelper(context: Context) {
*/ */
fun findAcceptedRootAndDiscardOthers(sourceId: Long, chapters: List<Chapter>): Single<Triple<ChapterChain, List<ChapterChain>, Boolean>> { fun findAcceptedRootAndDiscardOthers(sourceId: Long, chapters: List<Chapter>): Single<Triple<ChapterChain, List<ChapterChain>, Boolean>> {
// Find other chains // Find other chains
val chainsObservable = Observable.merge(chapters.map { chapter -> val chainsObservable = Observable.merge(
db.getChapters(chapter.url).asRxSingle().toObservable() chapters.map { chapter ->
}).toList().map { allChapters -> db.getChapters(chapter.url).asRxSingle().toObservable()
}
).toList().map { allChapters ->
allChapters.flatMap { innerChapters -> innerChapters.map { it.manga_id!! } }.distinct() allChapters.flatMap { innerChapters -> innerChapters.map { it.manga_id!! } }.distinct()
}.flatMap { mangaIds -> }.flatMap { mangaIds ->
Observable.merge( Observable.merge(
mangaIds.map { mangaId -> mangaIds.map { mangaId ->
Single.zip( Single.zip(
db.getManga(mangaId).asRxSingle(), db.getManga(mangaId).asRxSingle(),
db.getChaptersByMangaId(mangaId).asRxSingle() db.getChaptersByMangaId(mangaId).asRxSingle()
) { manga, chapters -> ) { manga, chapters ->
ChapterChain(manga, chapters) ChapterChain(manga, chapters)
}.toObservable().filter { }.toObservable().filter {
it.manga.source == sourceId it.manga.source == sourceId
}
} }
}
) )
}.toList() }.toList()
@ -66,65 +68,66 @@ class EHentaiUpdateHelper(context: Context) {
// Copy chain chapters to curChapters // Copy chain chapters to curChapters
val newChapters = toDiscard val newChapters = toDiscard
.flatMap { chain -> .flatMap { chain ->
val meta by lazy { val meta by lazy {
db.getFlatMetadataForManga(chain.manga.id!!) db.getFlatMetadataForManga(chain.manga.id!!)
.executeAsBlocking() .executeAsBlocking()
?.raise<EHentaiSearchMetadata>() ?.raise<EHentaiSearchMetadata>()
}
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) {
chapter.name = meta!!.title!!
chapter.date_upload = meta!!.datePosted!!
}
chapter
}
} }
.fold(accepted.chapters) { curChapters, chapter ->
val existing = curChapters.find { it.url == chapter.url }
val newLastPageRead = chainsAsChapters.maxBy { it.last_page_read }?.last_page_read chain.chapters.map { chapter ->
// Convert old style chapters to new style chapters if possible
if (existing != null) { if (chapter.date_upload <= 0 &&
existing.read = existing.read || chapter.read meta?.datePosted != null &&
existing.last_page_read = existing.last_page_read.coerceAtLeast(chapter.last_page_read) meta?.title != null
if (newLastPageRead != null && existing.last_page_read <= 0) { ) {
existing.last_page_read = newLastPageRead chapter.name = meta!!.title!!
} chapter.date_upload = meta!!.datePosted!!
existing.bookmark = existing.bookmark || chapter.bookmark
curChapters
} else if (chapter.date_upload > 0) { // Ignore chapters using the old system
new = true
curChapters + ChapterImpl().apply {
manga_id = accepted.manga.id
url = chapter.url
name = chapter.name
read = chapter.read
bookmark = chapter.bookmark
last_page_read = chapter.last_page_read
if (newLastPageRead != null && last_page_read <= 0) {
last_page_read = newLastPageRead
}
date_fetch = chapter.date_fetch
date_upload = chapter.date_upload
}
} else curChapters
}
.filter { it.date_upload > 0 } // Ignore chapters using the old system (filter after to prevent dupes from insert)
.sortedBy { it.date_upload }
.apply {
mapIndexed { index, chapter ->
chapter.name = "v${index + 1}: " + chapter.name.substringAfter(" ")
chapter.chapter_number = index + 1f
chapter.source_order = lastIndex - index
} }
chapter
} }
}
.fold(accepted.chapters) { curChapters, chapter ->
val existing = curChapters.find { it.url == chapter.url }
val newLastPageRead = chainsAsChapters.maxBy { it.last_page_read }?.last_page_read
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) {
existing.last_page_read = newLastPageRead
}
existing.bookmark = existing.bookmark || chapter.bookmark
curChapters
} else if (chapter.date_upload > 0) { // Ignore chapters using the old system
new = true
curChapters + ChapterImpl().apply {
manga_id = accepted.manga.id
url = chapter.url
name = chapter.name
read = chapter.read
bookmark = chapter.bookmark
last_page_read = chapter.last_page_read
if (newLastPageRead != null && last_page_read <= 0) {
last_page_read = newLastPageRead
}
date_fetch = chapter.date_fetch
date_upload = chapter.date_upload
}
} else curChapters
}
.filter { it.date_upload > 0 } // Ignore chapters using the old system (filter after to prevent dupes from insert)
.sortedBy { it.date_upload }
.apply {
mapIndexed { index, chapter ->
chapter.name = "v${index + 1}: " + chapter.name.substringAfter(" ")
chapter.chapter_number = index + 1f
chapter.source_order = lastIndex - index
}
}
toDiscard.forEach { it.manga.favorite = false } toDiscard.forEach { it.manga.favorite = false }
accepted.manga.favorite = true accepted.manga.favorite = true
@ -165,8 +168,8 @@ data class GalleryEntry(val gId: String, val gToken: String) {
override fun read(string: String): GalleryEntry { override fun read(string: String): GalleryEntry {
val colonIndex = string.indexOf(':') val colonIndex = string.indexOf(':')
return GalleryEntry( return GalleryEntry(
string.substring(0, colonIndex), string.substring(0, colonIndex),
string.substring(colonIndex + 1, string.length) string.substring(colonIndex + 1, string.length)
) )
} }
} }

View File

@ -137,17 +137,19 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
logger.d("Filtering manga and raising metadata...") logger.d("Filtering manga and raising metadata...")
val curTime = System.currentTimeMillis() val curTime = System.currentTimeMillis()
val allMeta = metadataManga.asFlow().cancellable().mapNotNull { manga -> val allMeta = metadataManga.asFlow().cancellable().mapNotNull { manga ->
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) {
return@mapNotNull null return@mapNotNull null
}
val meta = db.getFlatMetadataForManga(manga.id!!).asRxSingle().await() val meta = db.getFlatMetadataForManga(manga.id!!).asRxSingle().await()
?: return@mapNotNull null ?: return@mapNotNull null
val raisedMeta = meta.raise<EHentaiSearchMetadata>() val raisedMeta = meta.raise<EHentaiSearchMetadata>()
// Don't update galleries too frequently // Don't update galleries too frequently
if (raisedMeta.aged || (curTime - raisedMeta.lastUpdateCheck < MIN_BACKGROUND_UPDATE_FREQ && DebugToggles.RESTRICT_EXH_GALLERY_UPDATE_CHECK_FREQUENCY.enabled)) if (raisedMeta.aged || (curTime - raisedMeta.lastUpdateCheck < MIN_BACKGROUND_UPDATE_FREQ && DebugToggles.RESTRICT_EXH_GALLERY_UPDATE_CHECK_FREQUENCY.enabled)) {
return@mapNotNull null return@mapNotNull null
}
val chapter = db.getChaptersByMangaId(manga.id!!).asRxSingle().await().minBy { val chapter = db.getChaptersByMangaId(manga.id!!).asRxSingle().await().minBy {
it.date_upload it.date_upload
@ -172,13 +174,15 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
break break
} }
logger.d("Updating gallery (index: %s, manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s, modifiedThisIteration.size: %s)...", logger.d(
index, "Updating gallery (index: %s, manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s, modifiedThisIteration.size: %s)...",
manga.id, index,
meta.gId, manga.id,
meta.gToken, meta.gId,
failuresThisIteration, meta.gToken,
modifiedThisIteration.size) failuresThisIteration,
modifiedThisIteration.size
)
if (manga.id in modifiedThisIteration) { if (manga.id in modifiedThisIteration) {
// We already processed this manga! // We already processed this manga!
@ -194,32 +198,37 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
failuresThisIteration++ failuresThisIteration++
logger.e("> Network error while updating gallery!", e) logger.e("> Network error while updating gallery!", e)
logger.e("> (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)", logger.e(
manga.id, "> (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)",
meta.gId, manga.id,
meta.gToken, meta.gId,
failuresThisIteration) meta.gToken,
failuresThisIteration
)
} }
continue continue
} }
if (chapters.isEmpty()) { if (chapters.isEmpty()) {
logger.e("No chapters found for gallery (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)!", logger.e(
manga.id, "No chapters found for gallery (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)!",
meta.gId, manga.id,
meta.gToken, meta.gId,
failuresThisIteration) meta.gToken,
failuresThisIteration
)
continue continue
} }
// Find accepted root and discard others // Find accepted root and discard others
val (acceptedRoot, discardedRoots, hasNew) = val (acceptedRoot, discardedRoots, hasNew) =
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await() updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) || if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) ||
(hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })) { (hasNew && updatedManga.none { it.id == acceptedRoot.manga.id })
) {
updatedManga += acceptedRoot.manga updatedManga += acceptedRoot.manga
} }
@ -229,13 +238,13 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
} }
} finally { } finally {
prefs.eh_autoUpdateStats().set( prefs.eh_autoUpdateStats().set(
gson.toJson( gson.toJson(
EHentaiUpdaterStats( EHentaiUpdaterStats(
startTime, startTime,
allMeta.size, allMeta.size,
updatedThisIteration updatedThisIteration
)
) )
)
) )
if (updatedManga.isNotEmpty()) { if (updatedManga.isNotEmpty()) {
@ -247,7 +256,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
// New, current // New, current
suspend fun updateEntryAndGetChapters(manga: Manga): Pair<List<Chapter>, List<Chapter>> { suspend fun updateEntryAndGetChapters(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
val source = sourceManager.get(manga.source) as? EHentai val source = sourceManager.get(manga.source) as? EHentai
?: throw GalleryNotUpdatedException(false, IllegalStateException("Missing EH-based source (${manga.source})!")) ?: throw GalleryNotUpdatedException(false, IllegalStateException("Missing EH-based source (${manga.source})!"))
try { try {
val updatedManga = source.fetchMangaDetails(manga).toSingle().await(Schedulers.io()) val updatedManga = source.fetchMangaDetails(manga).toSingle().await(Schedulers.io())
@ -288,8 +297,10 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder { private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder {
return JobInfo.Builder( return JobInfo.Builder(
if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST
else JOB_ID_UPDATE_BACKGROUND, componentName()) else JOB_ID_UPDATE_BACKGROUND,
componentName()
)
} }
private fun Context.periodicBackgroundJobInfo( private fun Context.periodicBackgroundJobInfo(
@ -298,29 +309,32 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
requireUnmetered: Boolean requireUnmetered: Boolean
): JobInfo { ): JobInfo {
return baseBackgroundJobInfo(false) return baseBackgroundJobInfo(false)
.setPeriodic(period) .setPeriodic(period)
.setPersisted(true) .setPersisted(true)
.setRequiredNetworkType( .setRequiredNetworkType(
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
else JobInfo.NETWORK_TYPE_ANY) else JobInfo.NETWORK_TYPE_ANY
.apply { )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { .apply {
setRequiresBatteryNotLow(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
} setRequiresBatteryNotLow(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setEstimatedNetworkBytes(15000L * UPDATES_PER_ITERATION,
1000L * UPDATES_PER_ITERATION)
}
} }
.setRequiresCharging(requireCharging) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setEstimatedNetworkBytes(
15000L * UPDATES_PER_ITERATION,
1000L * UPDATES_PER_ITERATION
)
}
}
.setRequiresCharging(requireCharging)
// .setRequiresDeviceIdle(true) Job never seems to run with this // .setRequiresDeviceIdle(true) Job never seems to run with this
.build() .build()
} }
private fun Context.testBackgroundJobInfo(): JobInfo { private fun Context.testBackgroundJobInfo(): JobInfo {
return baseBackgroundJobInfo(true) return baseBackgroundJobInfo(true)
.setOverrideDeadline(1) .setOverrideDeadline(1)
.build() .build()
} }
fun launchBackgroundTest(context: Context) { fun launchBackgroundTest(context: Context) {
@ -343,9 +357,9 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
val wifiRestriction = "wifi" in restrictions val wifiRestriction = "wifi" in restrictions
val jobInfo = context.periodicBackgroundJobInfo( val jobInfo = context.periodicBackgroundJobInfo(
interval.hours.inMilliseconds.longValue, interval.hours.inMilliseconds.longValue,
acRestriction, acRestriction,
wifiRestriction wifiRestriction
) )
if (context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) { if (context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {

View File

@ -10,15 +10,16 @@ class FavoritesIntroDialog {
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
fun show(context: Context) = MaterialDialog(context) fun show(context: Context) = MaterialDialog(context)
.title(text = "IMPORTANT FAVORITES SYNC NOTES") .title(text = "IMPORTANT FAVORITES SYNC NOTES")
.message(text = HtmlCompat.fromHtml(FAVORITES_INTRO_TEXT, HtmlCompat.FROM_HTML_MODE_LEGACY)) .message(text = HtmlCompat.fromHtml(FAVORITES_INTRO_TEXT, HtmlCompat.FROM_HTML_MODE_LEGACY))
.positiveButton(android.R.string.ok) { .positiveButton(android.R.string.ok) {
prefs.eh_showSyncIntro().set(false) prefs.eh_showSyncIntro().set(false)
} }
.cancelable(false) .cancelable(false)
.show() .show()
private val FAVORITES_INTRO_TEXT = """ private val FAVORITES_INTRO_TEXT =
"""
1. Changes to category names in the app are <b>NOT</b> synced! Please <i>change the category names on ExHentai instead</i>. The category names will be copied from the ExHentai servers every sync. 1. Changes to category names in the app are <b>NOT</b> synced! Please <i>change the category names on ExHentai instead</i>. The category names will be copied from the ExHentai servers every sync.
<br><br> <br><br>
2. The favorite categories on ExHentai correspond to the <b>first 10 categories in the app</b> (excluding the 'Default' category). <i>Galleries in other categories will <b>NOT</b> be synced!</i> 2. The favorite categories on ExHentai correspond to the <b>first 10 categories in the app</b> (excluding the 'Default' category). <i>Galleries in other categories will <b>NOT</b> be synced!</i>
@ -30,5 +31,5 @@ class FavoritesIntroDialog {
5. <b>Do NOT put favorites in multiple categories</b> (the app supports this). This can confuse the sync algorithm as ExHentai only allows each favorite to be in one category. 5. <b>Do NOT put favorites in multiple categories</b> (the app supports this). This can confuse the sync algorithm as ExHentai only allows each favorite to be in one category.
<br><br> <br><br>
This dialog will only popup once. You can read these notes again by going to 'Settings > E-Hentai > Show favorites sync notes'. This dialog will only popup once. You can read these notes again by going to 'Settings > E-Hentai > Show favorites sync notes'.
""".trimIndent() """.trimIndent()
} }

View File

@ -39,7 +39,7 @@ class FavoritesSyncHelper(val context: Context) {
private val exh by lazy { private val exh by lazy {
Injekt.get<SourceManager>().get(EXH_SOURCE_ID) as? EHentai Injekt.get<SourceManager>().get(EXH_SOURCE_ID) as? EHentai
?: EHentai(0, true, context) ?: EHentai(0, true, context)
} }
private val storage = LocalFavoritesStorage() private val storage = LocalFavoritesStorage()
@ -82,8 +82,10 @@ class FavoritesSyncHelper(val context: Context) {
if (it.id in seenManga) { if (it.id in seenManga) {
val inCategories = db.getCategoriesForManga(it).executeAsBlocking() val inCategories = db.getCategoriesForManga(it).executeAsBlocking()
status.onNext(FavoritesSyncStatus.BadLibraryState status.onNext(
.MangaInMultipleCategories(it, inCategories)) FavoritesSyncStatus.BadLibraryState
.MangaInMultipleCategories(it, inCategories)
)
logger.w("Manga %s is in multiple categories!", it.id) logger.w("Manga %s is in multiple categories!", it.id)
return return
} else { } else {
@ -107,13 +109,17 @@ class FavoritesSyncHelper(val context: Context) {
// Take wake + wifi locks // Take wake + wifi locks
ignore { wakeLock?.release() } ignore { wakeLock?.release() }
wakeLock = ignore { wakeLock = ignore {
context.powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, context.powerManager.newWakeLock(
"teh:ExhFavoritesSyncWakelock") PowerManager.PARTIAL_WAKE_LOCK,
"teh:ExhFavoritesSyncWakelock"
)
} }
ignore { wifiLock?.release() } ignore { wifiLock?.release() }
wifiLock = ignore { wifiLock = ignore {
context.wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, context.wifiManager.createWifiLock(
"teh:ExhFavoritesSyncWifi") WifiManager.WIFI_MODE_FULL,
"teh:ExhFavoritesSyncWifi"
)
} }
// Do not update galleries while syncing favorites // Do not update galleries while syncing favorites
@ -137,8 +143,9 @@ class FavoritesSyncHelper(val context: Context) {
// Apply change sets // Apply change sets
applyChangeSetToLocal(errorList, remoteChanges) applyChangeSetToLocal(errorList, remoteChanges)
if (localChanges != null) if (localChanges != null) {
applyChangeSetToRemote(errorList, localChanges) applyChangeSetToRemote(errorList, localChanges)
}
status.onNext(FavoritesSyncStatus.Processing("Cleaning up")) status.onNext(FavoritesSyncStatus.Processing("Cleaning up"))
storage.snapshotEntries(realm) storage.snapshotEntries(realm)
@ -173,10 +180,11 @@ class FavoritesSyncHelper(val context: Context) {
EHentaiUpdateWorker.scheduleBackground(context) EHentaiUpdateWorker.scheduleBackground(context)
} }
if (errorList.isEmpty()) if (errorList.isEmpty()) {
status.onNext(FavoritesSyncStatus.Idle()) status.onNext(FavoritesSyncStatus.Idle())
else } else {
status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList)) status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList))
}
} }
private fun applyRemoteCategories(errorList: MutableList<String>, categories: List<String>) { private fun applyRemoteCategories(errorList: MutableList<String>, categories: List<String>) {
@ -217,22 +225,25 @@ class FavoritesSyncHelper(val context: Context) {
} }
// Only insert categories if changed // Only insert categories if changed
if (changed) if (changed) {
db.insertCategories(newLocalCategories).executeAsBlocking() db.insertCategories(newLocalCategories).executeAsBlocking()
}
} }
private fun addGalleryRemote(errorList: MutableList<String>, gallery: FavoriteEntry) { private fun addGalleryRemote(errorList: MutableList<String>, gallery: FavoriteEntry) {
val url = "${exh.baseUrl}/gallerypopups.php?gid=${gallery.gid}&t=${gallery.token}&act=addfav" val url = "${exh.baseUrl}/gallerypopups.php?gid=${gallery.gid}&t=${gallery.token}&act=addfav"
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.post(FormBody.Builder() .post(
.add("favcat", gallery.category.toString()) FormBody.Builder()
.add("favnote", "") .add("favcat", gallery.category.toString())
.add("apply", "Add to Favorites") .add("favnote", "")
.add("update", "1") .add("apply", "Add to Favorites")
.build()) .add("update", "1")
.build() .build()
)
.build()
if (!explicitlyRetryExhRequest(10, request)) { if (!explicitlyRetryExhRequest(10, request)) {
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!" val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
@ -271,8 +282,8 @@ class FavoritesSyncHelper(val context: Context) {
status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server")) status.onNext(FavoritesSyncStatus.Processing("Removing ${changeSet.removed.size} galleries from remote server"))
val formBody = FormBody.Builder() val formBody = FormBody.Builder()
.add("ddact", "delete") .add("ddact", "delete")
.add("apply", "Apply") .add("apply", "Apply")
// Add change set to form // Add change set to form
changeSet.removed.forEach { changeSet.removed.forEach {
@ -280,9 +291,9 @@ class FavoritesSyncHelper(val context: Context) {
} }
val request = Request.Builder() val request = Request.Builder()
.url("https://exhentai.org/favorites.php") .url("https://exhentai.org/favorites.php")
.post(formBody.build()) .post(formBody.build())
.build() .build()
if (!explicitlyRetryExhRequest(10, request)) { if (!explicitlyRetryExhRequest(10, request)) {
val errorString = "Unable to delete galleries from the remote servers!" val errorString = "Unable to delete galleries from the remote servers!"
@ -299,8 +310,12 @@ class FavoritesSyncHelper(val context: Context) {
// Apply additions // Apply additions
throttleManager.resetThrottle() throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it -> changeSet.added.forEachIndexed { index, it ->
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to remote server", status.onNext(
needWarnThrottle())) FavoritesSyncStatus.Processing(
"Adding gallery ${index + 1} of ${changeSet.added.size} to remote server",
needWarnThrottle()
)
)
throttleManager.throttle() throttleManager.throttle()
@ -317,8 +332,10 @@ class FavoritesSyncHelper(val context: Context) {
val url = it.getUrl() val url = it.getUrl()
// Consider both EX and EH sources // Consider both EX and EH sources
listOf(db.getManga(url, EXH_SOURCE_ID), listOf(
db.getManga(url, EH_SOURCE_ID)).forEach { db.getManga(url, EXH_SOURCE_ID),
db.getManga(url, EH_SOURCE_ID)
).forEach {
val manga = it.executeAsBlocking() val manga = it.executeAsBlocking()
if (manga?.favorite == true) { if (manga?.favorite == true) {
@ -340,16 +357,22 @@ class FavoritesSyncHelper(val context: Context) {
// Apply additions // Apply additions
throttleManager.resetThrottle() throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it -> changeSet.added.forEachIndexed { index, it ->
status.onNext(FavoritesSyncStatus.Processing("Adding gallery ${index + 1} of ${changeSet.added.size} to local library", status.onNext(
needWarnThrottle())) FavoritesSyncStatus.Processing(
"Adding gallery ${index + 1} of ${changeSet.added.size} to local library",
needWarnThrottle()
)
)
throttleManager.throttle() throttleManager.throttle()
// Import using gallery adder // Import using gallery adder
val result = galleryAdder.addGallery("${exh.baseUrl}${it.getUrl()}", val result = galleryAdder.addGallery(
true, "${exh.baseUrl}${it.getUrl()}",
exh, true,
throttleManager::throttle) exh,
throttleManager::throttle
)
if (result is GalleryAddEvent.Fail) { if (result is GalleryAddEvent.Fail) {
if (result is GalleryAddEvent.Fail.NotFound) { if (result is GalleryAddEvent.Fail.NotFound) {
@ -370,8 +393,10 @@ class FavoritesSyncHelper(val context: Context) {
throw IgnoredException() throw IgnoredException()
} }
} else if (result is GalleryAddEvent.Success) { } else if (result is GalleryAddEvent.Success) {
insertedMangaCategories += MangaCategory.create(result.manga, insertedMangaCategories += MangaCategory.create(
categories[it.category]) to result.manga result.manga,
categories[it.category]
) to result.manga
} }
} }
@ -379,12 +404,12 @@ class FavoritesSyncHelper(val context: Context) {
insertedMangaCategories.chunked(10).map { insertedMangaCategories.chunked(10).map {
Pair(it.map { it.first }, it.map { it.second }) Pair(it.map { it.first }, it.map { it.second })
}.forEach { }.forEach {
db.setMangaCategories(it.first, it.second) db.setMangaCategories(it.first, it.second)
} }
} }
fun needWarnThrottle() = fun needWarnThrottle() =
throttleManager.throttleTime >= THROTTLE_WARN throttleManager.throttleTime >= THROTTLE_WARN
class IgnoredException : RuntimeException() class IgnoredException : RuntimeException()
@ -401,12 +426,15 @@ sealed class FavoritesSyncStatus(val message: String) {
val manga: Manga, val manga: Manga,
val categories: List<Category> val categories: List<Category>
) : ) :
BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!") BadLibraryState("The gallery: ${manga.title} is in more than one category (${categories.joinToString { it.name }})!")
} }
class Initializing : FavoritesSyncStatus("Initializing sync") class Initializing : FavoritesSyncStatus("Initializing sync")
class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(if (isThrottle) class Processing(message: String, isThrottle: Boolean = false) : FavoritesSyncStatus(
"$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete." if (isThrottle) {
else "$message\n\nSync is currently throttling (to avoid being banned from ExHentai) and may take a long time to complete."
message) } else {
message
}
)
class CompleteWithErrors(messages: List<String>) : FavoritesSyncStatus(messages.joinToString("\n")) class CompleteWithErrors(messages: List<String>) : FavoritesSyncStatus(messages.joinToString("\n"))
} }

View File

@ -14,41 +14,46 @@ class LocalFavoritesStorage {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val realmConfig = RealmConfiguration.Builder() private val realmConfig = RealmConfiguration.Builder()
.name("fav-sync") .name("fav-sync")
.deleteRealmIfMigrationNeeded() .deleteRealmIfMigrationNeeded()
.build() .build()
fun getRealm() = Realm.getInstance(realmConfig) fun getRealm() = Realm.getInstance(realmConfig)
fun getChangedDbEntries(realm: Realm) = fun getChangedDbEntries(realm: Realm) =
getChangedEntries(realm, getChangedEntries(
realm,
parseToFavoriteEntries( parseToFavoriteEntries(
loadDbCategories( loadDbCategories(
db.getFavoriteMangas() db.getFavoriteMangas()
.executeAsBlocking() .executeAsBlocking()
.asSequence() .asSequence()
) )
) )
) )
fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) = fun getChangedRemoteEntries(realm: Realm, entries: List<EHentai.ParsedManga>) =
getChangedEntries(realm, getChangedEntries(
realm,
parseToFavoriteEntries( parseToFavoriteEntries(
entries.asSequence().map { entries.asSequence().map {
Pair(it.fav, it.manga.apply { Pair(
it.fav,
it.manga.apply {
favorite = true favorite = true
}) }
} )
}
) )
) )
fun snapshotEntries(realm: Realm) { fun snapshotEntries(realm: Realm) {
val dbMangas = parseToFavoriteEntries( val dbMangas = parseToFavoriteEntries(
loadDbCategories( loadDbCategories(
db.getFavoriteMangas() db.getFavoriteMangas()
.executeAsBlocking() .executeAsBlocking()
.asSequence() .asSequence()
) )
) )
// Delete old snapshot // Delete old snapshot
@ -70,29 +75,29 @@ class LocalFavoritesStorage {
} }
val removed = realm.where(FavoriteEntry::class.java) val removed = realm.where(FavoriteEntry::class.java)
.findAll() .findAll()
.filter { .filter {
queryListForEntry(terminated, it) == null queryListForEntry(terminated, it) == null
}.map { }.map {
realm.copyFromRealm(it) realm.copyFromRealm(it)
} }
return ChangeSet(added, removed) return ChangeSet(added, removed)
} }
private fun Realm.queryRealmForEntry(entry: FavoriteEntry) = private fun Realm.queryRealmForEntry(entry: FavoriteEntry) =
where(FavoriteEntry::class.java) where(FavoriteEntry::class.java)
.equalTo(FavoriteEntry::gid.name, entry.gid) .equalTo(FavoriteEntry::gid.name, entry.gid)
.equalTo(FavoriteEntry::token.name, entry.token) .equalTo(FavoriteEntry::token.name, entry.token)
.equalTo(FavoriteEntry::category.name, entry.category) .equalTo(FavoriteEntry::category.name, entry.category)
.findFirst() .findFirst()
private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) = private fun queryListForEntry(list: List<FavoriteEntry>, entry: FavoriteEntry) =
list.find { list.find {
it.gid == entry.gid && it.gid == entry.gid &&
it.token == entry.token && it.token == entry.token &&
it.category == entry.category it.category == entry.category
} }
private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> { private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> {
val dbCategories = db.getCategories().executeAsBlocking() val dbCategories = db.getCategories().executeAsBlocking()
@ -100,28 +105,34 @@ class LocalFavoritesStorage {
return manga.filter(this::validateDbManga).mapNotNull { return manga.filter(this::validateDbManga).mapNotNull {
val category = db.getCategoriesForManga(it).executeAsBlocking() val category = db.getCategoriesForManga(it).executeAsBlocking()
Pair(dbCategories.indexOf(category.firstOrNull() Pair(
?: return@mapNotNull null), it) dbCategories.indexOf(
category.firstOrNull()
?: return@mapNotNull null
),
it
)
} }
} }
private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) = private fun parseToFavoriteEntries(manga: Sequence<Pair<Int, Manga>>) =
manga.filter { manga.filter {
validateDbManga(it.second) validateDbManga(it.second)
}.mapNotNull { }.mapNotNull {
FavoriteEntry().apply { FavoriteEntry().apply {
title = it.second.title title = it.second.title
gid = EHentaiSearchMetadata.galleryId(it.second.url) gid = EHentaiSearchMetadata.galleryId(it.second.url)
token = EHentaiSearchMetadata.galleryToken(it.second.url) token = EHentaiSearchMetadata.galleryToken(it.second.url)
category = it.first category = it.first
if (this.category > MAX_CATEGORIES) if (this.category > MAX_CATEGORIES) {
return@mapNotNull null return@mapNotNull null
} }
} }
}
private fun validateDbManga(manga: Manga) = private fun validateDbManga(manga: Manga) =
manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) manga.favorite && (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID)
companion object { companion object {
const val MAX_CATEGORIES = 9 const val MAX_CATEGORIES = 9

View File

@ -71,39 +71,43 @@ class HitomiNozomi(
} }
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> { private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
if (data == null) if (data == null) {
return Single.just(emptyList()) return Single.just(emptyList())
}
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data" val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
val (offset, length) = data val (offset, length) = data
if (length > 100000000 || length <= 0) if (length > 100000000 || length <= 0) {
return Single.just(emptyList()) return Single.just(emptyList())
}
return client.newCall(rangedGet(url, offset, offset + length - 1)) return client.newCall(rangedGet(url, offset, offset + length - 1))
.asObservable() .asObservable()
.map { .map {
it.body?.bytes() ?: ByteArray(0) it.body?.bytes() ?: ByteArray(0)
}
.onErrorReturn { ByteArray(0) }
.map { inbuf ->
if (inbuf.isEmpty()) {
return@map emptyList<Int>()
} }
.onErrorReturn { ByteArray(0) }
.map { inbuf ->
if (inbuf.isEmpty())
return@map emptyList<Int>()
val view = ByteCursor(inbuf) val view = ByteCursor(inbuf)
val numberOfGalleryIds = view.nextInt() val numberOfGalleryIds = view.nextInt()
val expectedLength = numberOfGalleryIds * 4 + 4 val expectedLength = numberOfGalleryIds * 4 + 4
if (numberOfGalleryIds > 10000000 || if (numberOfGalleryIds > 10000000 ||
numberOfGalleryIds <= 0 || numberOfGalleryIds <= 0 ||
inbuf.size != expectedLength) { inbuf.size != expectedLength
return@map emptyList<Int>() ) {
} return@map emptyList<Int>()
}
(1..numberOfGalleryIds).map { (1..numberOfGalleryIds).map {
view.nextInt() view.nextInt()
} }
}.toSingle() }.toSingle()
} }
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> { private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
@ -112,10 +116,11 @@ class HitomiNozomi(
for (i in 0 until top) { for (i in 0 until top) {
val dv1i = dv1[i].toInt() and 0xFF val dv1i = dv1[i].toInt() and 0xFF
val dv2i = dv2[i].toInt() and 0xFF val dv2i = dv2[i].toInt() and 0xFF
if (dv1i < dv2i) if (dv1i < dv2i) {
return -1 return -1
else if (dv1i > dv2i) } else if (dv1i > dv2i) {
return 1 return 1
}
} }
return 0 return 0
} }
@ -185,16 +190,16 @@ class HitomiNozomi(
} }
return client.newCall(rangedGet(url, address, address + MAX_NODE_SIZE - 1)) return client.newCall(rangedGet(url, address, address + MAX_NODE_SIZE - 1))
.asObservableSuccess() .asObservableSuccess()
.map { .map {
it.body?.bytes() ?: ByteArray(0) it.body?.bytes() ?: ByteArray(0)
} }
.onErrorReturn { ByteArray(0) } .onErrorReturn { ByteArray(0) }
.map { nodedata -> .map { nodedata ->
if (nodedata.isNotEmpty()) { if (nodedata.isNotEmpty()) {
decodeNode(nodedata) decodeNode(nodedata)
} else null } else null
}.toSingle() }.toSingle()
} }
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> { fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> {
@ -203,17 +208,19 @@ class HitomiNozomi(
nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$tag-$language$NOZOMI_EXTENSION" nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$tag-$language$NOZOMI_EXTENSION"
} }
return client.newCall(Request.Builder() return client.newCall(
Request.Builder()
.url(nozomiAddress) .url(nozomiAddress)
.build()) .build()
.asObservableSuccess() )
.map { resp -> .asObservableSuccess()
val body = resp.body!!.bytes() .map { resp ->
val cursor = ByteCursor(body) val body = resp.body!!.bytes()
(1..body.size / 4).map { val cursor = ByteCursor(body)
cursor.nextInt() (1..body.size / 4).map {
} cursor.nextInt()
}.toSingle() }
}.toSingle()
} }
private fun hashTerm(query: String): HashedTerm { private fun hashTerm(query: String): HashedTerm {
@ -233,15 +240,18 @@ class HitomiNozomi(
private val HASH_CHARSET = Charsets.UTF_8 private val HASH_CHARSET = Charsets.UTF_8
fun rangedGet(url: String, rangeBegin: Long, rangeEnd: Long?): Request { fun rangedGet(url: String, rangeBegin: Long, rangeEnd: Long?): Request {
return GET(url, Headers.Builder() return GET(
url,
Headers.Builder()
.add("Range", "bytes=$rangeBegin-${rangeEnd ?: ""}") .add("Range", "bytes=$rangeBegin-${rangeEnd ?: ""}")
.build()) .build()
)
} }
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> { fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}")) return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
.asObservableSuccess() .asObservableSuccess()
.map { it.body!!.string().toLong() } .map { it.body!!.string().toLong() }
} }
} }
} }

View File

@ -32,8 +32,8 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
override fun createView(root: ViewGroup, textColor: Int, textSize: Float, textAlpha: Float): View { override fun createView(root: ViewGroup, textColor: Int, textSize: Float, textAlpha: Float): View {
val view = LinearLayout(root.context) val view = LinearLayout(root.context)
view.layoutParams = ViewGroup.LayoutParams( view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT ViewGroup.LayoutParams.WRAP_CONTENT
) )
view.setPadding(4.dpToPx, 0, 4.dpToPx, 4.dpToPx) view.setPadding(4.dpToPx, 0, 4.dpToPx, 4.dpToPx)
val textView = TextView(view.context) val textView = TextView(view.context)
@ -42,15 +42,16 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
textView.alpha = textAlpha textView.alpha = textAlpha
textView.text = HtmlCompat.fromHtml(buildInfo(), HtmlCompat.FROM_HTML_MODE_LEGACY) textView.text = HtmlCompat.fromHtml(buildInfo(), HtmlCompat.FROM_HTML_MODE_LEGACY)
textView.layoutParams = LinearLayout.LayoutParams( textView.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT ViewGroup.LayoutParams.WRAP_CONTENT
) )
view.addView(textView) view.addView(textView)
this.textView = textView this.textView = textView
return view return view
} }
fun buildInfo() = """ fun buildInfo() =
"""
<font color='green'>===[ ${context.getString(R.string.app_name)} ]===</font><br> <font color='green'>===[ ${context.getString(R.string.app_name)} ]===</font><br>
<b>Build type:</b> ${BuildConfig.BUILD_TYPE}<br> <b>Build type:</b> ${BuildConfig.BUILD_TYPE}<br>
<b>Debug mode:</b> ${BuildConfig.DEBUG.asEnabledString()}<br> <b>Debug mode:</b> ${BuildConfig.DEBUG.asEnabledString()}<br>
@ -58,7 +59,7 @@ class EHDebugModeOverlay(private val context: Context) : OverlayModule<String>(n
<b>Commit SHA:</b> ${BuildConfig.COMMIT_SHA}<br> <b>Commit SHA:</b> ${BuildConfig.COMMIT_SHA}<br>
<b>Log level:</b> ${EHLogLevel.currentLogLevel.name.toLowerCase()}<br> <b>Log level:</b> ${EHLogLevel.currentLogLevel.name.toLowerCase()}<br>
<b>Source blacklist:</b> ${prefs.eh_enableSourceBlacklist().get().asEnabledString()} <b>Source blacklist:</b> ${prefs.eh_enableSourceBlacklist().get().asEnabledString()}
""".trimIndent() """.trimIndent()
private fun Boolean.asEnabledString() = if (this) "enabled" else "disabled" private fun Boolean.asEnabledString() = if (this) "enabled" else "disabled"
} }

View File

@ -16,7 +16,7 @@ enum class EHLogLevel(val description: String) {
fun init(context: Context) { fun init(context: Context) {
curLogLevel = PreferenceManager.getDefaultSharedPreferences(context) curLogLevel = PreferenceManager.getDefaultSharedPreferences(context)
.getInt(PreferenceKeys.eh_logLevel, 0) .getInt(PreferenceKeys.eh_logLevel, 0)
} }
fun shouldLog(requiredLogLevel: EHLogLevel): Boolean { fun shouldLog(requiredLogLevel: EHLogLevel): Boolean {

View File

@ -35,31 +35,32 @@ fun parseHumanReadableByteCount(arg0: String): Double? {
return null return null
} }
fun String?.nullIfBlank(): String? = if (isNullOrBlank()) fun String?.nullIfBlank(): String? = if (isNullOrBlank()) {
null null
else } else {
this this
}
fun <K, V> Set<Map.Entry<K, V>>.forEach(action: (K, V) -> Unit) { fun <K, V> Set<Map.Entry<K, V>>.forEach(action: (K, V) -> Unit) {
forEach { action(it.key, it.value) } forEach { action(it.key, it.value) }
} }
val ONGOING_SUFFIX = arrayOf( val ONGOING_SUFFIX = arrayOf(
"[ongoing]", "[ongoing]",
"(ongoing)", "(ongoing)",
"{ongoing}", "{ongoing}",
"<ongoing>", "<ongoing>",
"ongoing", "ongoing",
"[incomplete]", "[incomplete]",
"(incomplete)", "(incomplete)",
"{incomplete}", "{incomplete}",
"<incomplete>", "<incomplete>",
"incomplete", "incomplete",
"[wip]", "[wip]",
"(wip)", "(wip)",
"{wip}", "{wip}",
"<wip>", "<wip>",
"wip" "wip"
) )
val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US) val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)

View File

@ -50,10 +50,11 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
thumbnailUrl?.let { manga.thumbnail_url = it } thumbnailUrl?.let { manga.thumbnail_url = it }
// No title bug? // No title bug?
val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault()) val titleObj = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().getOrDefault()) {
altTitle ?: title altTitle ?: title
else } else {
title title
}
titleObj?.let { manga.title = it } titleObj?.let { manga.title = it }
// Set artist (if we can find one) // Set artist (if we can find one)
@ -102,8 +103,8 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {
@ -117,24 +118,25 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
private const val EH_ARTIST_NAMESPACE = "artist" private const val EH_ARTIST_NAMESPACE = "artist"
private fun splitGalleryUrl(url: String) = private fun splitGalleryUrl(url: String) =
url.let { url.let {
// Only parse URL if is full URL // Only parse URL if is full URL
val pathSegments = if (it.startsWith("http")) val pathSegments = if (it.startsWith("http")) {
Uri.parse(it).pathSegments Uri.parse(it).pathSegments
else } else {
it.split('/') it.split('/')
pathSegments.filterNot(String::isNullOrBlank) }
} pathSegments.filterNot(String::isNullOrBlank)
}
fun galleryId(url: String) = splitGalleryUrl(url)[1] fun galleryId(url: String) = splitGalleryUrl(url)[1]
fun galleryToken(url: String) = fun galleryToken(url: String) =
splitGalleryUrl(url)[2] splitGalleryUrl(url)[2]
fun normalizeUrl(url: String) = fun normalizeUrl(url: String) =
idAndTokenToUrl(galleryId(url), galleryToken(url)) idAndTokenToUrl(galleryId(url), galleryToken(url))
fun idAndTokenToUrl(id: String, token: String) = fun idAndTokenToUrl(id: String, token: String) =
"/g/$id/$token/?nw=always" "/g/$id/$token/?nw=always"
} }
} }

View File

@ -32,8 +32,8 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
manga.description = listOf(titleDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {

View File

@ -34,8 +34,8 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
manga.description = listOf(titleDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {

View File

@ -31,15 +31,15 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
manga.status = SManga.UNKNOWN manga.status = SManga.UNKNOWN
val detailsDesc = "Title: $title\n" + val detailsDesc = "Title: $title\n" +
"Artist: $artist\n" "Artist: $artist\n"
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
manga.genre = tagsToGenreString() manga.genre = tagsToGenreString()
manga.description = listOf(detailsDesc, tagsDesc.toString()) manga.description = listOf(detailsDesc, tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {
@ -50,6 +50,6 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
const val BASE_URL = "https://hentai.cafe" const val BASE_URL = "https://hentai.cafe"
fun hcIdFromUrl(url: String) = fun hcIdFromUrl(url: String) =
url.split("/").last { it.isNotBlank() } url.split("/").last { it.isNotBlank() }
} }
} }

View File

@ -62,11 +62,13 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
detailsDesc += "Language: ${it.capitalize()}\n" detailsDesc += "Language: ${it.capitalize()}\n"
} }
if (series.isNotEmpty()) if (series.isNotEmpty()) {
detailsDesc += "Series: ${series.joinToString()}\n" detailsDesc += "Series: ${series.joinToString()}\n"
}
if (characters.isNotEmpty()) if (characters.isNotEmpty()) {
detailsDesc += "Characters: ${characters.joinToString()}\n" detailsDesc += "Characters: ${characters.joinToString()}\n"
}
uploadDate?.let { uploadDate?.let {
detailsDesc += "Upload date: ${EX_DATE_FORMAT.format(Date(it))}\n" detailsDesc += "Upload date: ${EX_DATE_FORMAT.format(Date(it))}\n"
@ -80,8 +82,8 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {
@ -93,9 +95,9 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
const val BASE_URL = "https://hitomi.la" const val BASE_URL = "https://hitomi.la"
fun hlIdFromUrl(url: String) = fun hlIdFromUrl(url: String) =
url.split('/').last().split('-').last().substringBeforeLast('.') url.split('/').last().split('-').last().substringBeforeLast('.')
fun urlFromHlId(id: String) = fun urlFromHlId(id: String) =
"$BASE_URL/galleries/$id.html" "$BASE_URL/galleries/$id.html"
} }
} }

View File

@ -44,9 +44,11 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
if (mediaId != null) { if (mediaId != null) {
val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault() val hqThumbs = Injekt.get<PreferencesHelper>().eh_nh_useHighQualityThumbs().getOrDefault()
typeToExtension(if (hqThumbs) coverImageType else thumbnailImageType)?.let { typeToExtension(if (hqThumbs) coverImageType else thumbnailImageType)?.let {
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if (hqThumbs) manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/${if (hqThumbs) {
"cover" "cover"
else "thumb"}.$it" } else {
"thumb"
}}.$it"
} }
} }
@ -91,8 +93,8 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {
@ -108,14 +110,14 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
private const val NHENTAI_CATEGORIES_NAMESPACE = "category" private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
fun typeToExtension(t: String?) = fun typeToExtension(t: String?) =
when (t) { when (t) {
"p" -> "png" "p" -> "png"
"j" -> "jpg" "j" -> "jpg"
else -> null else -> null
} }
fun nhUrlToId(url: String) = fun nhUrlToId(url: String) =
url.split("/").last { it.isNotBlank() }.toLong() url.split("/").last { it.isNotBlank() }.toLong()
fun nhIdToPath(id: Long) = "/g/$id/" fun nhIdToPath(id: Long) = "/g/$id/"
} }

View File

@ -41,11 +41,12 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
manga.title = it manga.title = it
titleDesc += "Title: $it\n" titleDesc += "Title: $it\n"
} }
if (altTitles.isNotEmpty()) if (altTitles.isNotEmpty()) {
titleDesc += "Alternate Titles: \n" + altTitles titleDesc += "Alternate Titles: \n" + altTitles
.joinToString(separator = "\n", postfix = "\n") { .joinToString(separator = "\n", postfix = "\n") {
"$it" "$it"
} }
}
val detailsDesc = StringBuilder() val detailsDesc = StringBuilder()
artist?.let { artist?.let {
@ -76,8 +77,8 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {
@ -87,9 +88,9 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
const val TAG_TYPE_DEFAULT = 0 const val TAG_TYPE_DEFAULT = 0
private fun splitGalleryUrl(url: String) = private fun splitGalleryUrl(url: String) =
url.let { url.let {
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank) Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
} }
fun pvIdFromUrl(url: String) = splitGalleryUrl(url).last() fun pvIdFromUrl(url: String) = splitGalleryUrl(url).last()
} }
@ -102,7 +103,7 @@ enum class PervEdenLang(val id: Long) {
companion object { companion object {
fun source(id: Long) = fun source(id: Long) =
values().find { it.id == id } values().find { it.id == id }
?: throw IllegalArgumentException("Unknown source ID: $id!") ?: throw IllegalArgumentException("Unknown source ID: $id!")
} }
} }

View File

@ -55,8 +55,8 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {

View File

@ -65,8 +65,8 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
val tagsDesc = tagsToDescription() val tagsDesc = tagsToDescription()
manga.description = listOf(titleDesc, detailsDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc, detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank) .filter(String::isNotBlank)
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
companion object { companion object {
@ -77,7 +77,7 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
val BASE_URL = "https://www.tsumino.com" val BASE_URL = "https://www.tsumino.com"
fun tmIdFromUrl(url: String) = fun tmIdFromUrl(url: String) =
Uri.parse(url).lastPathSegment Uri.parse(url).lastPathSegment
fun mangaUrlFromId(id: String) = "/Book/Info/$id" fun mangaUrlFromId(id: String) = "/Book/Info/$id"

View File

@ -18,9 +18,9 @@ data class FlatMetadata(
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>) = fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>) =
RaisedSearchMetadata.raiseFlattenGson RaisedSearchMetadata.raiseFlattenGson
.fromJson(metadata.extra, clazz.java).apply { .fromJson(metadata.extra, clazz.java).apply {
fillBaseFields(this@FlatMetadata) fillBaseFields(this@FlatMetadata)
} }
} }
fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedOperation<FlatMetadata?> { fun DatabaseHelper.getFlatMetadataForManga(mangaId: Long): PreparedOperation<FlatMetadata?> {

View File

@ -36,29 +36,29 @@ abstract class RaisedSearchMetadata {
abstract fun copyTo(manga: SManga) abstract fun copyTo(manga: SManga)
fun tagsToGenreString() = fun tagsToGenreString() =
tags.filter { it.type != TAG_TYPE_VIRTUAL } tags.filter { it.type != TAG_TYPE_VIRTUAL }
.joinToString { (if (it.namespace != null) "${it.namespace}: " else "") + it.name } .joinToString { (if (it.namespace != null) "${it.namespace}: " else "") + it.name }
fun tagsToDescription() = fun tagsToDescription() =
StringBuilder("Tags:\n").apply { StringBuilder("Tags:\n").apply {
// BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags' // BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy { val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
it.namespace it.namespace
}.entries }.entries
groupedTags.forEach { namespace, tags -> groupedTags.forEach { namespace, tags ->
if (tags.isNotEmpty()) { if (tags.isNotEmpty()) {
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" }) val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
if (namespace != null) { if (namespace != null) {
this += "" this += ""
this += namespace this += namespace
this += ": " this += ": "
}
this += joinedTags
this += "\n"
} }
this += joinedTags
this += "\n"
} }
} }
}
fun List<RaisedTag>.ofNamespace(ns: String): List<RaisedTag> { fun List<RaisedTag>.ofNamespace(ns: String): List<RaisedTag> {
return filter { it.namespace == ns } return filter { it.namespace == ns }
@ -76,23 +76,23 @@ abstract class RaisedSearchMetadata {
indexedExtra, indexedExtra,
0 0
), ),
tags.map { tags.map {
SearchTag( SearchTag(
null, null,
mangaId, mangaId,
it.namespace, it.namespace,
it.name, it.name,
it.type it.type
) )
}, },
titles.map { titles.map {
SearchTitle( SearchTitle(
null, null,
mangaId, mangaId,
it.title, it.title,
it.type it.type
) )
} }
) )
} }
@ -126,7 +126,7 @@ abstract class RaisedSearchMetadata {
* @return the property value. * @return the property value.
*/ */
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) = override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) =
thisRef.getTitleOfType(type) thisRef.getTitleOfType(type)
/** /**
* Sets the value of the property for the given object. * Sets the value of the property for the given object.
@ -135,7 +135,7 @@ abstract class RaisedSearchMetadata {
* @param value the value to set. * @param value the value to set.
*/ */
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) = override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) =
thisRef.replaceTitleOfType(type, value) thisRef.replaceTitleOfType(type, value)
} }
} }
} }

View File

@ -18,22 +18,22 @@ import exh.metadata.sql.tables.SearchTagTable.COL_TYPE
import exh.metadata.sql.tables.SearchTagTable.TABLE import exh.metadata.sql.tables.SearchTagTable.TABLE
class SearchTagTypeMapping : SQLiteTypeMapping<SearchTag>( class SearchTagTypeMapping : SQLiteTypeMapping<SearchTag>(
SearchTagPutResolver(), SearchTagPutResolver(),
SearchTagGetResolver(), SearchTagGetResolver(),
SearchTagDeleteResolver() SearchTagDeleteResolver()
) )
class SearchTagPutResolver : DefaultPutResolver<SearchTag>() { class SearchTagPutResolver : DefaultPutResolver<SearchTag>() {
override fun mapToInsertQuery(obj: SearchTag) = InsertQuery.builder() override fun mapToInsertQuery(obj: SearchTag) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: SearchTag) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: SearchTag) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: SearchTag) = ContentValues(5).apply { override fun mapToContentValues(obj: SearchTag) = ContentValues(5).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -47,19 +47,19 @@ class SearchTagPutResolver : DefaultPutResolver<SearchTag>() {
class SearchTagGetResolver : DefaultGetResolver<SearchTag>() { class SearchTagGetResolver : DefaultGetResolver<SearchTag>() {
override fun mapFromCursor(cursor: Cursor): SearchTag = SearchTag( override fun mapFromCursor(cursor: Cursor): SearchTag = SearchTag(
id = cursor.getLong(cursor.getColumnIndex(COL_ID)), id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)), mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
namespace = cursor.getString(cursor.getColumnIndex(COL_NAMESPACE)), namespace = cursor.getString(cursor.getColumnIndex(COL_NAMESPACE)),
name = cursor.getString(cursor.getColumnIndex(COL_NAME)), name = cursor.getString(cursor.getColumnIndex(COL_NAME)),
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE)) type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
) )
} }
class SearchTagDeleteResolver : DefaultDeleteResolver<SearchTag>() { class SearchTagDeleteResolver : DefaultDeleteResolver<SearchTag>() {
override fun mapToDeleteQuery(obj: SearchTag) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: SearchTag) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -17,22 +17,22 @@ import exh.metadata.sql.tables.SearchTitleTable.COL_TYPE
import exh.metadata.sql.tables.SearchTitleTable.TABLE import exh.metadata.sql.tables.SearchTitleTable.TABLE
class SearchTitleTypeMapping : SQLiteTypeMapping<SearchTitle>( class SearchTitleTypeMapping : SQLiteTypeMapping<SearchTitle>(
SearchTitlePutResolver(), SearchTitlePutResolver(),
SearchTitleGetResolver(), SearchTitleGetResolver(),
SearchTitleDeleteResolver() SearchTitleDeleteResolver()
) )
class SearchTitlePutResolver : DefaultPutResolver<SearchTitle>() { class SearchTitlePutResolver : DefaultPutResolver<SearchTitle>() {
override fun mapToInsertQuery(obj: SearchTitle) = InsertQuery.builder() override fun mapToInsertQuery(obj: SearchTitle) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: SearchTitle) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: SearchTitle) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: SearchTitle) = ContentValues(4).apply { override fun mapToContentValues(obj: SearchTitle) = ContentValues(4).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -45,18 +45,18 @@ class SearchTitlePutResolver : DefaultPutResolver<SearchTitle>() {
class SearchTitleGetResolver : DefaultGetResolver<SearchTitle>() { class SearchTitleGetResolver : DefaultGetResolver<SearchTitle>() {
override fun mapFromCursor(cursor: Cursor): SearchTitle = SearchTitle( override fun mapFromCursor(cursor: Cursor): SearchTitle = SearchTitle(
id = cursor.getLong(cursor.getColumnIndex(COL_ID)), id = cursor.getLong(cursor.getColumnIndex(COL_ID)),
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)), mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
title = cursor.getString(cursor.getColumnIndex(COL_TITLE)), title = cursor.getString(cursor.getColumnIndex(COL_TITLE)),
type = cursor.getInt(cursor.getColumnIndex(COL_TYPE)) type = cursor.getInt(cursor.getColumnIndex(COL_TYPE))
) )
} }
class SearchTitleDeleteResolver : DefaultDeleteResolver<SearchTitle>() { class SearchTitleDeleteResolver : DefaultDeleteResolver<SearchTitle>() {
override fun mapToDeleteQuery(obj: SearchTitle) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: SearchTitle) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

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

View File

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

View File

@ -9,21 +9,25 @@ import exh.metadata.sql.tables.SearchTagTable
interface SearchTagQueries : DbProvider { interface SearchTagQueries : DbProvider {
fun getSearchTagsForManga(mangaId: Long) = db.get() fun getSearchTagsForManga(mangaId: Long) = db.get()
.listOfObjects(SearchTag::class.java) .listOfObjects(SearchTag::class.java)
.withQuery(Query.builder() .withQuery(
.table(SearchTagTable.TABLE) Query.builder()
.where("${SearchTagTable.COL_MANGA_ID} = ?") .table(SearchTagTable.TABLE)
.whereArgs(mangaId) .where("${SearchTagTable.COL_MANGA_ID} = ?")
.build()) .whereArgs(mangaId)
.prepare() .build()
)
.prepare()
fun deleteSearchTagsForManga(mangaId: Long) = db.delete() fun deleteSearchTagsForManga(mangaId: Long) = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(SearchTagTable.TABLE) DeleteQuery.builder()
.where("${SearchTagTable.COL_MANGA_ID} = ?") .table(SearchTagTable.TABLE)
.whereArgs(mangaId) .where("${SearchTagTable.COL_MANGA_ID} = ?")
.build()) .whereArgs(mangaId)
.prepare() .build()
)
.prepare()
fun insertSearchTag(searchTag: SearchTag) = db.put().`object`(searchTag).prepare() fun insertSearchTag(searchTag: SearchTag) = db.put().`object`(searchTag).prepare()
@ -31,10 +35,12 @@ interface SearchTagQueries : DbProvider {
fun deleteSearchTag(searchTag: SearchTag) = db.delete().`object`(searchTag).prepare() fun deleteSearchTag(searchTag: SearchTag) = db.delete().`object`(searchTag).prepare()
fun deleteAllSearchTags() = db.delete().byQuery(DeleteQuery.builder() fun deleteAllSearchTags() = db.delete().byQuery(
DeleteQuery.builder()
.table(SearchTagTable.TABLE) .table(SearchTagTable.TABLE)
.build()) .build()
.prepare() )
.prepare()
fun setSearchTagsForManga(mangaId: Long, tags: List<SearchTag>) { fun setSearchTagsForManga(mangaId: Long, tags: List<SearchTag>) {
db.inTransaction { db.inTransaction {

View File

@ -9,21 +9,25 @@ import exh.metadata.sql.tables.SearchTitleTable
interface SearchTitleQueries : DbProvider { interface SearchTitleQueries : DbProvider {
fun getSearchTitlesForManga(mangaId: Long) = db.get() fun getSearchTitlesForManga(mangaId: Long) = db.get()
.listOfObjects(SearchTitle::class.java) .listOfObjects(SearchTitle::class.java)
.withQuery(Query.builder() .withQuery(
.table(SearchTitleTable.TABLE) Query.builder()
.where("${SearchTitleTable.COL_MANGA_ID} = ?") .table(SearchTitleTable.TABLE)
.whereArgs(mangaId) .where("${SearchTitleTable.COL_MANGA_ID} = ?")
.build()) .whereArgs(mangaId)
.prepare() .build()
)
.prepare()
fun deleteSearchTitlesForManga(mangaId: Long) = db.delete() fun deleteSearchTitlesForManga(mangaId: Long) = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(SearchTitleTable.TABLE) DeleteQuery.builder()
.where("${SearchTitleTable.COL_MANGA_ID} = ?") .table(SearchTitleTable.TABLE)
.whereArgs(mangaId) .where("${SearchTitleTable.COL_MANGA_ID} = ?")
.build()) .whereArgs(mangaId)
.prepare() .build()
)
.prepare()
fun insertSearchTitle(searchTitle: SearchTitle) = db.put().`object`(searchTitle).prepare() fun insertSearchTitle(searchTitle: SearchTitle) = db.put().`object`(searchTitle).prepare()
@ -31,10 +35,12 @@ interface SearchTitleQueries : DbProvider {
fun deleteSearchTitle(searchTitle: SearchTitle) = db.delete().`object`(searchTitle).prepare() fun deleteSearchTitle(searchTitle: SearchTitle) = db.delete().`object`(searchTitle).prepare()
fun deleteAllSearchTitle() = db.delete().byQuery(DeleteQuery.builder() fun deleteAllSearchTitle() = db.delete().byQuery(
DeleteQuery.builder()
.table(SearchTitleTable.TABLE) .table(SearchTitleTable.TABLE)
.build()) .build()
.prepare() )
.prepare()
fun setSearchTitlesForManga(mangaId: Long, titles: List<SearchTitle>) { fun setSearchTitlesForManga(mangaId: Long, titles: List<SearchTitle>) {
db.inTransaction { db.inTransaction {

View File

@ -17,7 +17,8 @@ object SearchMetadataTable {
// Insane foreign, primary key to avoid touch manga table // Insane foreign, primary key to avoid touch manga table
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_MANGA_ID INTEGER NOT NULL PRIMARY KEY, $COL_MANGA_ID INTEGER NOT NULL PRIMARY KEY,
$COL_UPLOADER TEXT, $COL_UPLOADER TEXT,
$COL_EXTRA TEXT NOT NULL, $COL_EXTRA TEXT NOT NULL,

View File

@ -16,7 +16,8 @@ object SearchTagTable {
const val COL_TYPE = "type" const val COL_TYPE = "type"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_NAMESPACE TEXT, $COL_NAMESPACE TEXT,

View File

@ -14,7 +14,8 @@ object SearchTitleTable {
const val COL_TYPE = "type" const val COL_TYPE = "type"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_TITLE TEXT NOT NULL, $COL_TITLE TEXT NOT NULL,

View File

@ -9,13 +9,14 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
private val HIDE_SCRIPT = """ private val HIDE_SCRIPT =
document.querySelector("#forgot_button").style.visibility = "hidden"; """
document.querySelector("#signup_button").style.visibility = "hidden"; document.querySelector("#forgot_button").style.visibility = "hidden";
document.querySelector("#announcement").style.visibility = "hidden"; document.querySelector("#signup_button").style.visibility = "hidden";
document.querySelector("nav").style.visibility = "hidden"; document.querySelector("#announcement").style.visibility = "hidden";
document.querySelector("footer").style.visibility = "hidden"; document.querySelector("nav").style.visibility = "hidden";
""".trimIndent() document.querySelector("footer").style.visibility = "hidden";
""".trimIndent()
private fun verifyComplete(url: String): Boolean { private fun verifyComplete(url: String): Boolean {
return url.toHttpUrlOrNull()?.let { parsed -> return url.toHttpUrlOrNull()?.let { parsed ->
@ -28,14 +29,14 @@ val MANGADEX_LOGIN_PATCH: EHInterceptor = { request, response, sourceId ->
response.interceptAsHtml { doc -> response.interceptAsHtml { doc ->
if (doc.title().trim().equals("Login - MangaDex", true)) { if (doc.title().trim().equals("Login - MangaDex", true)) {
BrowserActionActivity.launchAction( BrowserActionActivity.launchAction(
Injekt.get<Application>(), Injekt.get<Application>(),
::verifyComplete, ::verifyComplete,
HIDE_SCRIPT, HIDE_SCRIPT,
"https://mangadex.org/login", "https://mangadex.org/login",
"Login", "Login",
(Injekt.get<SourceManager>().get(sourceId) as? HttpSource)?.headers?.toMultimap()?.mapValues { (Injekt.get<SourceManager>().get(sourceId) as? HttpSource)?.headers?.toMultimap()?.mapValues {
it.value.joinToString(",") it.value.joinToString(",")
} ?: emptyMap() } ?: emptyMap()
) )
} }
} }
@ -43,43 +44,43 @@ val MANGADEX_LOGIN_PATCH: EHInterceptor = { request, response, sourceId ->
} }
val MANGADEX_SOURCE_IDS = listOf( val MANGADEX_SOURCE_IDS = listOf(
2499283573021220255, 2499283573021220255,
8033579885162383068, 8033579885162383068,
1952071260038453057, 1952071260038453057,
2098905203823335614, 2098905203823335614,
5098537545549490547, 5098537545549490547,
4505830566611664829, 4505830566611664829,
9194073792736219759, 9194073792736219759,
6400665728063187402, 6400665728063187402,
4938773340256184018, 4938773340256184018,
5860541308324630662, 5860541308324630662,
5189216366882819742, 5189216366882819742,
2655149515337070132, 2655149515337070132,
1145824452519314725, 1145824452519314725,
3846770256925560569, 3846770256925560569,
3807502156582598786, 3807502156582598786,
4284949320785450865, 4284949320785450865,
5463447640980279236, 5463447640980279236,
8578871918181236609, 8578871918181236609,
6750440049024086587, 6750440049024086587,
3339599426223341161, 3339599426223341161,
5148895169070562838, 5148895169070562838,
1493666528525752601, 1493666528525752601,
1713554459881080228, 1713554459881080228,
4150470519566206911, 4150470519566206911,
1347402746269051958, 1347402746269051958,
3578612018159256808, 3578612018159256808,
425785191804166217, 425785191804166217,
8254121249433835847, 8254121249433835847,
3260701926561129943, 3260701926561129943,
1411768577036936240, 1411768577036936240,
3285208643537017688, 3285208643537017688,
737986167355114438, 737986167355114438,
1471784905273036181, 1471784905273036181,
5967745367608513818, 5967745367608513818,
3781216447842245147, 3781216447842245147,
4774459486579224459, 4774459486579224459,
4710920497926776490, 4710920497926776490,
5779037855201976894 5779037855201976894
) )
const val MANGADEX_DOMAIN = "mangadex.org" const val MANGADEX_DOMAIN = "mangadex.org"

View File

@ -16,8 +16,10 @@ fun OkHttpClient.Builder.injectPatches(sourceIdProducer: () -> Long): OkHttpClie
} }
fun findAndApplyPatches(sourceId: Long): EHInterceptor { fun findAndApplyPatches(sourceId: Long): EHInterceptor {
return ((EH_INTERCEPTORS[sourceId] ?: emptyList()) + return (
(EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR] ?: emptyList())).merge() (EH_INTERCEPTORS[sourceId] ?: emptyList()) +
(EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR] ?: emptyList())
).merge()
} }
fun List<EHInterceptor>.merge(): EHInterceptor { fun List<EHInterceptor>.merge(): EHInterceptor {
@ -30,12 +32,12 @@ fun List<EHInterceptor>.merge(): EHInterceptor {
private const val EH_UNIVERSAL_INTERCEPTOR = -1L private const val EH_UNIVERSAL_INTERCEPTOR = -1L
private val EH_INTERCEPTORS: Map<Long, List<EHInterceptor>> = mapOf( private val EH_INTERCEPTORS: Map<Long, List<EHInterceptor>> = mapOf(
EH_UNIVERSAL_INTERCEPTOR to listOf( EH_UNIVERSAL_INTERCEPTOR to listOf(
CAPTCHA_DETECTION_PATCH // Auto captcha detection CAPTCHA_DETECTION_PATCH // Auto captcha detection
), ),
// MangaDex login support // MangaDex login support
*MANGADEX_SOURCE_IDS.map { id -> *MANGADEX_SOURCE_IDS.map { id ->
id to listOf(MANGADEX_LOGIN_PATCH) id to listOf(MANGADEX_LOGIN_PATCH)
}.toTypedArray() }.toTypedArray()
) )

View File

@ -13,9 +13,9 @@ val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) { if (doc.getElementsByClass("g-recaptcha").isNotEmpty()) {
// Found it, allow the user to solve this thing // Found it, allow the user to solve this thing
BrowserActionActivity.launchUniversal( BrowserActionActivity.launchUniversal(
Injekt.get<Application>(), Injekt.get<Application>(),
sourceId, sourceId,
request.url.toString() request.url.toString()
) )
} }
} }

View File

@ -12,10 +12,11 @@ class SearchEngine {
component: Text? component: Text?
): Pair<String, List<String>>? { ): Pair<String, List<String>>? {
val maybeLenientComponent = component?.let { val maybeLenientComponent = component?.let {
if (!it.exact) if (!it.exact) {
it.asLenientTagQueries() it.asLenientTagQueries()
else } else {
listOf(it.asQuery()) listOf(it.asQuery())
}
} }
val componentTagQuery = maybeLenientComponent?.let { val componentTagQuery = maybeLenientComponent?.let {
val params = mutableListOf<String>() val params = mutableListOf<String>()
@ -25,11 +26,12 @@ class SearchEngine {
}.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params }.joinToString(separator = " OR ", prefix = "(", postfix = ")") to params
} }
return if (namespace != null) { return if (namespace != null) {
var query = """ var query =
"""
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE} (SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
AND ${SearchTagTable.COL_NAMESPACE} LIKE ? AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
""".trimIndent() """.trimIndent()
val params = mutableListOf(escapeLike(namespace)) val params = mutableListOf(escapeLike(namespace))
if (componentTagQuery != null) { if (componentTagQuery != null) {
query += "\n AND ${componentTagQuery.first}" query += "\n AND ${componentTagQuery.first}"
@ -39,18 +41,20 @@ class SearchEngine {
"$query)" to params "$query)" to params
} else if (component != null) { } else if (component != null) {
// Match title + tags // Match title + tags
val tagQuery = """ val tagQuery =
"""
SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE} SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
WHERE ${componentTagQuery!!.first} WHERE ${componentTagQuery!!.first}
""".trimIndent() to componentTagQuery.second """.trimIndent() to componentTagQuery.second
val titleQuery = """ val titleQuery =
"""
SELECT ${SearchTitleTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTitleTable.TABLE} SELECT ${SearchTitleTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTitleTable.TABLE}
WHERE ${SearchTitleTable.COL_TITLE} LIKE ? WHERE ${SearchTitleTable.COL_TITLE} LIKE ?
""".trimIndent() to listOf(component.asLenientTitleQuery()) """.trimIndent() to listOf(component.asLenientTitleQuery())
"(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to "(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to
(tagQuery.second + titleQuery.second) (tagQuery.second + titleQuery.second)
} else null } else null
} }
@ -86,22 +90,25 @@ class SearchEngine {
} }
val completeParams = mutableListOf<String>() val completeParams = mutableListOf<String>()
var baseQuery = """ var baseQuery =
"""
SELECT ${SearchMetadataTable.COL_MANGA_ID} SELECT ${SearchMetadataTable.COL_MANGA_ID}
FROM ${SearchMetadataTable.TABLE} meta FROM ${SearchMetadataTable.TABLE} meta
""".trimIndent() """.trimIndent()
include.forEachIndexed { index, pair -> include.forEachIndexed { index, pair ->
baseQuery += "\n" + (""" baseQuery += "\n" + (
"""
INNER JOIN ${pair.first} i$index INNER JOIN ${pair.first} i$index
ON i$index.$COL_MANGA_ID = meta.${SearchMetadataTable.COL_MANGA_ID} ON i$index.$COL_MANGA_ID = meta.${SearchMetadataTable.COL_MANGA_ID}
""".trimIndent()) """.trimIndent()
)
completeParams += pair.second completeParams += pair.second
} }
exclude.forEach { exclude.forEach {
wheres += """ wheres += """
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first}) (meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
""".trimIndent() """.trimIndent()
whereParams += it.second whereParams += it.second
} }
@ -196,8 +203,8 @@ class SearchEngine {
fun escapeLike(string: String): String { fun escapeLike(string: String): String {
return string.replace("\\", "\\\\") return string.replace("\\", "\\\\")
.replace("_", "\\_") .replace("_", "\\_")
.replace("%", "\\%") .replace("%", "\\%")
} }
} }
} }

View File

@ -28,13 +28,13 @@ class Text : QueryComponent() {
fun asLenientTagQueries(): List<String> { fun asLenientTagQueries(): List<String> {
if (lenientTagQueries == null) { if (lenientTagQueries == null) {
lenientTagQueries = listOf( lenientTagQueries = listOf(
// Match beginning of tag // Match beginning of tag
rBaseBuilder().append("%").toString(), rBaseBuilder().append("%").toString(),
// Tag word matcher (that matches multiple words) // Tag word matcher (that matches multiple words)
// Can't make it match a single word in Realm :( // Can't make it match a single word in Realm :(
StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(), StringBuilder(" ").append(rBaseBuilder()).append(" ").toString(),
StringBuilder(" ").append(rBaseBuilder()).toString(), StringBuilder(" ").append(rBaseBuilder()).toString(),
rBaseBuilder().append(" ").toString() rBaseBuilder().append(" ").toString()
) )
} }
return lenientTagQueries!! return lenientTagQueries!!
@ -52,11 +52,11 @@ class Text : QueryComponent() {
return builder return builder
} }
fun rawTextOnly() = if (rawText != null) fun rawTextOnly() = if (rawText != null) {
rawText!! rawText!!
else { } else {
rawText = components rawText = components
.joinToString(separator = "", transform = { it.rawText }) .joinToString(separator = "", transform = { it.rawText })
rawText!! rawText!!
} }

View File

@ -62,8 +62,9 @@ class SmartSearchEngine(
} else title } else title
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io()) val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
if (searchResults.mangas.size == 1) if (searchResults.mangas.size == 1) {
return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0)) return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0))
}
searchResults.mangas.map { searchResults.mangas.map {
val normalizedDistance = normalizedLevenshtein.similarity(title, it.title) val normalizedDistance = normalizedLevenshtein.similarity(title, it.title)

View File

@ -7,38 +7,38 @@ object BlacklistedSources {
val PERVEDEN_EN_EXT_SOURCES = listOf(4673633799850248749) val PERVEDEN_EN_EXT_SOURCES = listOf(4673633799850248749)
val PERVEDEN_IT_EXT_SOURCES = listOf(1433898225963724122) val PERVEDEN_IT_EXT_SOURCES = listOf(1433898225963724122)
val EHENTAI_EXT_SOURCES = listOf( val EHENTAI_EXT_SOURCES = listOf(
8100626124886895451, 8100626124886895451,
57122881048805941, 57122881048805941,
4678440076103929247, 4678440076103929247,
1876021963378735852, 1876021963378735852,
3955189842350477641, 3955189842350477641,
4348288691341764259, 4348288691341764259,
773611868725221145, 773611868725221145,
5759417018342755550, 5759417018342755550,
825187715438990384, 825187715438990384,
6116711405602166104, 6116711405602166104,
7151438547982231541, 7151438547982231541,
2171445159732592630, 2171445159732592630,
3032959619549451093, 3032959619549451093,
5980349886941016589, 5980349886941016589,
6073266008352078708, 6073266008352078708,
5499077866612745456, 5499077866612745456,
6140480779421365791 6140480779421365791
) )
val BLACKLISTED_EXT_SOURCES = NHENTAI_EXT_SOURCES + val BLACKLISTED_EXT_SOURCES = NHENTAI_EXT_SOURCES +
PERVEDEN_EN_EXT_SOURCES + PERVEDEN_EN_EXT_SOURCES +
PERVEDEN_IT_EXT_SOURCES + PERVEDEN_IT_EXT_SOURCES +
EHENTAI_EXT_SOURCES EHENTAI_EXT_SOURCES
val BLACKLISTED_EXTENSIONS = listOf( val BLACKLISTED_EXTENSIONS = listOf(
"eu.kanade.tachiyomi.extension.all.ehentai", "eu.kanade.tachiyomi.extension.all.ehentai",
"eu.kanade.tachiyomi.extension.all.nhentai", "eu.kanade.tachiyomi.extension.all.nhentai",
"eu.kanade.tachiyomi.extension.en.perveden", "eu.kanade.tachiyomi.extension.en.perveden",
"eu.kanade.tachiyomi.extension.it.perveden" "eu.kanade.tachiyomi.extension.it.perveden"
) )
val HIDDEN_SOURCES = listOf( val HIDDEN_SOURCES = listOf(
MERGED_SOURCE_ID MERGED_SOURCE_ID
) )
} }

View File

@ -18,7 +18,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun popularMangaRequest(page: Int) = override fun popularMangaRequest(page: Int) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
@ -26,7 +26,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun popularMangaParse(response: Response) = override fun popularMangaParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Returns the request for the search manga given the page. * Returns the request for the search manga given the page.
@ -36,7 +36,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
@ -44,7 +44,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun searchMangaParse(response: Response) = override fun searchMangaParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Returns the request for latest manga given the page. * Returns the request for latest manga given the page.
@ -52,7 +52,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun latestUpdatesRequest(page: Int) = override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
@ -60,7 +60,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun latestUpdatesParse(response: Response) = override fun latestUpdatesParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns the details of a manga. * Parses the response from the site and returns the details of a manga.
@ -68,7 +68,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun mangaDetailsParse(response: Response) = override fun mangaDetailsParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a list of chapters. * Parses the response from the site and returns a list of chapters.
@ -76,7 +76,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun chapterListParse(response: Response) = override fun chapterListParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a list of pages. * Parses the response from the site and returns a list of pages.
@ -84,7 +84,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun pageListParse(response: Response) = override fun pageListParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns the absolute url to the source image. * Parses the response from the site and returns the absolute url to the source image.
@ -92,7 +92,7 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun imageUrlParse(response: Response) = override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Base url of the website without the trailing slash, like: http://mysite.com * Base url of the website without the trailing slash, like: http://mysite.com
@ -240,7 +240,8 @@ abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
private fun ensureDelegateCompatible() { private fun ensureDelegateCompatible() {
if (versionId != delegate.versionId || if (versionId != delegate.versionId ||
lang != delegate.lang) { lang != delegate.lang
) {
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang})!") throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang})!")
} }
} }

View File

@ -23,7 +23,7 @@ class EnhancedHttpSource(
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun popularMangaRequest(page: Int) = override fun popularMangaRequest(page: Int) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
@ -31,7 +31,7 @@ class EnhancedHttpSource(
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun popularMangaParse(response: Response) = override fun popularMangaParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Returns the request for the search manga given the page. * Returns the request for the search manga given the page.
@ -41,7 +41,7 @@ class EnhancedHttpSource(
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
@ -49,7 +49,7 @@ class EnhancedHttpSource(
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun searchMangaParse(response: Response) = override fun searchMangaParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Returns the request for latest manga given the page. * Returns the request for latest manga given the page.
@ -57,7 +57,7 @@ class EnhancedHttpSource(
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun latestUpdatesRequest(page: Int) = override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a [MangasPage] object. * Parses the response from the site and returns a [MangasPage] object.
@ -65,7 +65,7 @@ class EnhancedHttpSource(
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun latestUpdatesParse(response: Response) = override fun latestUpdatesParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns the details of a manga. * Parses the response from the site and returns the details of a manga.
@ -73,7 +73,7 @@ class EnhancedHttpSource(
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun mangaDetailsParse(response: Response) = override fun mangaDetailsParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a list of chapters. * Parses the response from the site and returns a list of chapters.
@ -81,7 +81,7 @@ class EnhancedHttpSource(
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun chapterListParse(response: Response) = override fun chapterListParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns a list of pages. * Parses the response from the site and returns a list of pages.
@ -89,7 +89,7 @@ class EnhancedHttpSource(
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun pageListParse(response: Response) = override fun pageListParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Parses the response from the site and returns the absolute url to the source image. * Parses the response from the site and returns the absolute url to the source image.
@ -97,7 +97,7 @@ class EnhancedHttpSource(
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun imageUrlParse(response: Response) = override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("Should never be called!") throw UnsupportedOperationException("Should never be called!")
/** /**
* Base url of the website without the trailing slash, like: http://mysite.com * Base url of the website without the trailing slash, like: http://mysite.com
@ -153,7 +153,7 @@ class EnhancedHttpSource(
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
source().fetchSearchManga(page, query, filters) source().fetchSearchManga(page, query, filters)
/** /**
* Returns an observable containing a page with a list of latest manga updates. * Returns an observable containing a page with a list of latest manga updates.
@ -209,7 +209,7 @@ class EnhancedHttpSource(
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
override fun prepareNewChapter(chapter: SChapter, manga: SManga) = override fun prepareNewChapter(chapter: SChapter, manga: SManga) =
source().prepareNewChapter(chapter, manga) source().prepareNewChapter(chapter, manga)
/** /**
* Returns the list of filters for the source. * Returns the list of filters for the source.

View File

@ -14,7 +14,7 @@ class ConfiguringDialogController : DialogController() {
private var materialDialog: MaterialDialog? = null private var materialDialog: MaterialDialog? = null
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
if (savedViewState == null) if (savedViewState == null) {
thread { thread {
try { try {
EHConfigurator().configureAll() EHConfigurator().configureAll()
@ -25,10 +25,10 @@ class ConfiguringDialogController : DialogController() {
activity?.let { activity?.let {
it.runOnUiThread { it.runOnUiThread {
MaterialDialog(it) MaterialDialog(it)
.title(text = "Configuration failed!") .title(text = "Configuration failed!")
.message(text = "An error occurred during the configuration process: " + e.message) .message(text = "An error occurred during the configuration process: " + e.message)
.positiveButton(android.R.string.ok) .positiveButton(android.R.string.ok)
.show() .show()
} }
} }
Timber.e(e, "Configuration error!") Timber.e(e, "Configuration error!")
@ -37,14 +37,15 @@ class ConfiguringDialogController : DialogController() {
finish() finish()
} }
} }
}
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.title(text = "Uploading settings to server") .title(text = "Uploading settings to server")
.message(text = "Please wait, this may take some time...") .message(text = "Please wait, this may take some time...")
.cancelable(false) .cancelable(false)
.also { .also {
materialDialog = it materialDialog = it
} }
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {

View File

@ -18,11 +18,11 @@ class EHConfigurator {
private val sources: SourceManager by injectLazy() private val sources: SourceManager by injectLazy()
private val configuratorClient = OkHttpClient.Builder() private val configuratorClient = OkHttpClient.Builder()
.maybeInjectEHLogger() .maybeInjectEHLogger()
.build() .build()
private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder() private fun EHentai.requestWithCreds(sp: Int = 1) = Request.Builder()
.addHeader("Cookie", cookiesHeader(sp)) .addHeader("Cookie", cookiesHeader(sp))
private fun EHentai.execProfileActions( private fun EHentai.execProfileActions(
action: String, action: String,
@ -30,15 +30,19 @@ class EHConfigurator {
set: String, set: String,
sp: Int sp: Int
) = ) =
configuratorClient.newCall(requestWithCreds(sp) configuratorClient.newCall(
requestWithCreds(sp)
.url(uconfigUrl) .url(uconfigUrl)
.post(FormBody.Builder() .post(
FormBody.Builder()
.add("profile_action", action) .add("profile_action", action)
.add("profile_name", name) .add("profile_name", name)
.add("profile_set", set) .add("profile_set", set)
.build()) .build()
.build()) )
.execute() .build()
)
.execute()
private val EHentai.uconfigUrl get() = baseUrl + UCONFIG_URL private val EHentai.uconfigUrl get() = baseUrl + UCONFIG_URL
@ -47,10 +51,12 @@ class EHConfigurator {
val exhSource = sources.get(EXH_SOURCE_ID) as EHentai val exhSource = sources.get(EXH_SOURCE_ID) as EHentai
// Get hath perks // Get hath perks
val perksPage = configuratorClient.newCall(ehSource.requestWithCreds() val perksPage = configuratorClient.newCall(
ehSource.requestWithCreds()
.url(HATH_PERKS_URL) .url(HATH_PERKS_URL)
.build()) .build()
.execute().asJsoup() )
.execute().asJsoup()
val hathPerks = EHHathPerksResponse() val hathPerks = EHHathPerksResponse()
@ -97,24 +103,29 @@ class EHConfigurator {
} }
// No profile slots left :( // No profile slots left :(
if (availableProfiles.isEmpty()) if (availableProfiles.isEmpty()) {
throw IllegalStateException("You are out of profile slots on ${source.name}, please delete a profile!") throw IllegalStateException("You are out of profile slots on ${source.name}, please delete a profile!")
}
// Create profile in available slot // Create profile in available slot
val slot = availableProfiles.first() val slot = availableProfiles.first()
val response = source.execProfileActions("create", val response = source.execProfileActions(
PROFILE_NAME, "create",
slot.toString(), PROFILE_NAME,
1) slot.toString(),
1
)
// Build new profile // Build new profile
val form = EhUConfigBuilder().build(hathPerks) val form = EhUConfigBuilder().build(hathPerks)
// Send new profile to server // Send new profile to server
configuratorClient.newCall(source.requestWithCreds(sp = slot) configuratorClient.newCall(
source.requestWithCreds(sp = slot)
.url(source.uconfigUrl) .url(source.uconfigUrl)
.post(form) .post(form)
.build()).execute() .build()
).execute()
// Persist slot + sk // Persist slot + sk
source.spPref().set(slot) source.spPref().set(slot)
@ -129,12 +140,15 @@ class EHConfigurator {
it.startsWith("hath_perks=") it.startsWith("hath_perks=")
}?.removePrefix("hath_perks=")?.substringBefore(';') }?.removePrefix("hath_perks=")?.substringBefore(';')
if (keyCookie != null) if (keyCookie != null) {
prefs.eh_settingsKey().set(keyCookie) prefs.eh_settingsKey().set(keyCookie)
if (sessionCookie != null) }
if (sessionCookie != null) {
prefs.eh_sessionCookie().set(sessionCookie) prefs.eh_sessionCookie().set(sessionCookie)
if (hathPerksCookie != null) }
if (hathPerksCookie != null) {
prefs.eh_hathPerksCookies().set(hathPerksCookie) prefs.eh_hathPerksCookies().set(hathPerksCookie)
}
} }
companion object { companion object {

View File

@ -11,9 +11,11 @@ class EhUConfigBuilder {
fun build(hathPerks: EHHathPerksResponse): FormBody { fun build(hathPerks: EHHathPerksResponse): FormBody {
val configItems = mutableListOf<ConfigItem>() val configItems = mutableListOf<ConfigItem>()
configItems += when (prefs.imageQuality() configItems += when (
prefs.imageQuality()
.getOrDefault() .getOrDefault()
.toLowerCase()) { .toLowerCase()
) {
"ovrs_2400" -> Entry.ImageSize.`2400` "ovrs_2400" -> Entry.ImageSize.`2400`
"ovrs_1600" -> Entry.ImageSize.`1600` "ovrs_1600" -> Entry.ImageSize.`1600`
"high" -> Entry.ImageSize.`1280` "high" -> Entry.ImageSize.`1280`
@ -23,20 +25,23 @@ class EhUConfigBuilder {
else -> Entry.ImageSize.AUTO else -> Entry.ImageSize.AUTO
} }
configItems += if (prefs.useHentaiAtHome().getOrDefault()) configItems += if (prefs.useHentaiAtHome().getOrDefault()) {
Entry.UseHentaiAtHome.YES Entry.UseHentaiAtHome.YES
else } else {
Entry.UseHentaiAtHome.NO Entry.UseHentaiAtHome.NO
}
configItems += if (prefs.useJapaneseTitle().getOrDefault()) configItems += if (prefs.useJapaneseTitle().getOrDefault()) {
Entry.TitleDisplayLanguage.JAPANESE Entry.TitleDisplayLanguage.JAPANESE
else } else {
Entry.TitleDisplayLanguage.DEFAULT Entry.TitleDisplayLanguage.DEFAULT
}
configItems += if (prefs.eh_useOriginalImages().getOrDefault()) configItems += if (prefs.eh_useOriginalImages().getOrDefault()) {
Entry.UseOriginalImages.YES Entry.UseOriginalImages.YES
else } else {
Entry.UseOriginalImages.NO Entry.UseOriginalImages.NO
}
configItems += when { configItems += when {
hathPerks.allThumbs -> Entry.ThumbnailRows.`40` hathPerks.allThumbs -> Entry.ThumbnailRows.`40`

View File

@ -14,25 +14,29 @@ class WarnConfigureDialogController : DialogController() {
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.title(text = "Settings profile note") .title(text = "Settings profile note")
.message(text = """ .message(
text =
"""
The app will now add a new settings profile on E-Hentai and ExHentai to optimize app performance. Please ensure that you have less than three profiles on both sites. The app will now add a new settings profile on E-Hentai and ExHentai to optimize app performance. Please ensure that you have less than three profiles on both sites.
If you have no idea what settings profiles are, then it probably doesn't matter, just hit 'OK'. If you have no idea what settings profiles are, then it probably doesn't matter, just hit 'OK'.
""".trimIndent()) """.trimIndent()
.positiveButton(android.R.string.ok) { )
prefs.eh_showSettingsUploadWarning().set(false) .positiveButton(android.R.string.ok) {
ConfiguringDialogController().showDialog(router) prefs.eh_showSettingsUploadWarning().set(false)
} ConfiguringDialogController().showDialog(router)
.cancelable(false) }
.cancelable(false)
} }
companion object { companion object {
fun uploadSettings(router: Router) { fun uploadSettings(router: Router) {
if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().get()) if (Injekt.get<PreferencesHelper>().eh_showSettingsUploadWarning().get()) {
WarnConfigureDialogController().showDialog(router) WarnConfigureDialogController().showDialog(router)
else } else {
ConfiguringDialogController().showDialog(router) ConfiguringDialogController().showDialog(router)
}
} }
} }
} }

View File

@ -49,66 +49,68 @@ class BatchAddController : NucleusController<EhFragmentBatchAddBinding, BatchAdd
val progressSubscriptions = CompositeSubscription() val progressSubscriptions = CompositeSubscription()
presenter.currentlyAddingRelay presenter.currentlyAddingRelay
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { .subscribeUntilDestroy {
progressSubscriptions.clear() progressSubscriptions.clear()
if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) { if (it == BatchAddPresenter.STATE_INPUT_TO_PROGRESS) {
showProgress(this) showProgress(this)
progressSubscriptions += presenter.progressRelay progressSubscriptions += presenter.progressRelay
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.combineLatest(presenter.progressTotalRelay) { progress, total -> .combineLatest(presenter.progressTotalRelay) { progress, total ->
// Show hide dismiss button // Show hide dismiss button
binding.progressDismissBtn.visibility = binding.progressDismissBtn.visibility =
if (progress == total) if (progress == total) {
View.VISIBLE View.VISIBLE
else View.GONE } else {
View.GONE
}
formatProgress(progress, total) formatProgress(progress, total)
}.subscribeUntilDestroy { }.subscribeUntilDestroy {
binding.progressText.text = it binding.progressText.text = it
} }
progressSubscriptions += presenter.progressTotalRelay progressSubscriptions += presenter.progressTotalRelay
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { .subscribeUntilDestroy {
binding.progressBar.max = it binding.progressBar.max = it
}
progressSubscriptions += presenter.progressRelay
.observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy {
binding.progressBar.progress = it
}
presenter.eventRelay
?.observeOn(AndroidSchedulers.mainThread())
?.subscribeUntilDestroy {
binding.progressLog.append("$it\n")
}?.let {
progressSubscriptions += it
} }
} else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
hideProgress(this) progressSubscriptions += presenter.progressRelay
presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE) .observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy {
binding.progressBar.progress = it
}
presenter.eventRelay
?.observeOn(AndroidSchedulers.mainThread())
?.subscribeUntilDestroy {
binding.progressLog.append("$it\n")
}?.let {
progressSubscriptions += it
} }
} else if (it == BatchAddPresenter.STATE_PROGRESS_TO_INPUT) {
hideProgress(this)
presenter.currentlyAddingRelay.call(BatchAddPresenter.STATE_IDLE)
} }
}
} }
} }
private val View.progressViews private val View.progressViews
get() = listOf( get() = listOf(
binding.progressTitleView, binding.progressTitleView,
binding.progressLogWrapper, binding.progressLogWrapper,
binding.progressBar, binding.progressBar,
binding.progressText, binding.progressText,
binding.progressDismissBtn binding.progressDismissBtn
) )
private val View.inputViews private val View.inputViews
get() = listOf( get() = listOf(
binding.inputTitleView, binding.inputTitleView,
binding.galleriesBox, binding.galleriesBox,
binding.btnAddGalleries binding.btnAddGalleries
) )
private var List<View>.visibility: Int private var List<View>.visibility: Int
@ -144,12 +146,12 @@ class BatchAddController : NucleusController<EhFragmentBatchAddBinding, BatchAdd
private fun noGalleriesSpecified() { private fun noGalleriesSpecified() {
activity?.let { activity?.let {
MaterialDialog(it) MaterialDialog(it)
.title(text = "No galleries to add!") .title(text = "No galleries to add!")
.message(text = "You must specify at least one gallery to add!") .message(text = "You must specify at least one gallery to add!")
.positiveButton(android.R.string.ok) { materialDialog -> materialDialog.dismiss() } .positiveButton(android.R.string.ok) { materialDialog -> materialDialog.dismiss() }
.cancelable(true) .cancelable(true)
.cancelOnTouchOutside(true) .cancelOnTouchOutside(true)
.show() .show()
} }
} }
} }

View File

@ -40,10 +40,14 @@ class BatchAddPresenter : BasePresenter<BatchAddController>() {
failed.add(s) failed.add(s)
} }
progressRelay.call(i + 1) progressRelay.call(i + 1)
eventRelay?.call((when (result) { eventRelay?.call(
is GalleryAddEvent.Success -> "[OK]" (
is GalleryAddEvent.Fail -> "[ERROR]" when (result) {
}) + " " + result.logMessage) is GalleryAddEvent.Success -> "[OK]"
is GalleryAddEvent.Fail -> "[ERROR]"
}
) + " " + result.logMessage
)
} }
// Show report // Show report

View File

@ -26,9 +26,9 @@ class AutoSolvingWebViewClient(
val doc = response.asJsoup() val doc = response.asJsoup()
doc.body().appendChild(Element("script").appendChild(DataNode(CROSS_WINDOW_SCRIPT_INNER))) doc.body().appendChild(Element("script").appendChild(DataNode(CROSS_WINDOW_SCRIPT_INNER)))
return WebResourceResponse( return WebResourceResponse(
"text/html", "text/html",
"UTF-8", "UTF-8",
doc.toString().byteInputStream(Charset.forName("UTF-8")).buffered() doc.toString().byteInputStream(Charset.forName("UTF-8")).buffered()
) )
} }
return super.shouldInterceptRequest(view, request) return super.shouldInterceptRequest(view, request)

View File

@ -14,8 +14,9 @@ open class BasicWebViewClient(
if (verifyComplete(url)) { if (verifyComplete(url)) {
activity.finish() activity.finish()
} else { } else {
if (injectScript != null) if (injectScript != null) {
view.evaluateJavascript("(function() {$injectScript})();", null) view.evaluateJavascript("(function() {$injectScript})();", null)
}
} }
} }
} }

View File

@ -62,24 +62,27 @@ class BrowserActionActivity : AppCompatActivity() {
val originalSource = if (sourceId != -1L) sourceManager.get(sourceId) else null val originalSource = if (sourceId != -1L) sourceManager.get(sourceId) else null
val source = if (originalSource != null) { val source = if (originalSource != null) {
originalSource as? ActionCompletionVerifier originalSource as? ActionCompletionVerifier
?: run { ?: run {
(originalSource as? HttpSource)?.let { (originalSource as? HttpSource)?.let {
NoopActionCompletionVerifier(it) NoopActionCompletionVerifier(it)
}
} }
}
} else null } else null
val headers = ((source as? HttpSource)?.headers?.toMultimap()?.mapValues { val headers = (
it.value.joinToString(",") (source as? HttpSource)?.headers?.toMultimap()?.mapValues {
} ?: emptyMap()) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap()) it.value.joinToString(",")
} ?: emptyMap()
) + (intent.getSerializableExtra(HEADERS_EXTRA) as? HashMap<String, String> ?: emptyMap())
val cookies: HashMap<String, String>? = val cookies: HashMap<String, String>? =
intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String> intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
val script: String? = intent.getStringExtra(SCRIPT_EXTRA) val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
val url: String? = intent.getStringExtra(URL_EXTRA) val url: String? = intent.getStringExtra(URL_EXTRA)
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA) val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
@Suppress("NOT_NULL_ASSERTION_ON_CALLABLE_REFERENCE") val verifyComplete = if (source != null) { @Suppress("NOT_NULL_ASSERTION_ON_CALLABLE_REFERENCE")
val verifyComplete = if (source != null) {
source::verifyComplete!! source::verifyComplete!!
} else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean } else intent.getSerializableExtra(VERIFY_LAMBDA_EXTRA) as? (String) -> Boolean
@ -139,10 +142,12 @@ class BrowserActionActivity : AppCompatActivity() {
webview.webViewClient = if (actionName == null && preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) { webview.webViewClient = if (actionName == null && preferencesHelper.eh_autoSolveCaptchas().getOrDefault()) {
// Fetch auto-solve credentials early for speed // Fetch auto-solve credentials early for speed
credentialsObservable = httpClient.newCall(Request.Builder() credentialsObservable = httpClient.newCall(
// Rob demo credentials Request.Builder()
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials") // Rob demo credentials
.build()) .url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
.build()
)
.asObservableSuccess() .asObservableSuccess()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.map { .map {
@ -176,12 +181,12 @@ class BrowserActionActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null) webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
MaterialDialog(this) MaterialDialog(this)
.title(text = "Captcha solve failure") .title(text = "Captcha solve failure")
.message(text = "Failed to auto-solve the captcha!") .message(text = "Failed to auto-solve the captcha!")
.cancelable(true) .cancelable(true)
.cancelOnTouchOutside(true) .cancelOnTouchOutside(true)
.positiveButton(android.R.string.ok) .positiveButton(android.R.string.ok)
.show() .show()
} }
} }
@ -192,13 +197,19 @@ class BrowserActionActivity : AppCompatActivity() {
when (stage) { when (stage) {
STAGE_CHECKBOX -> { STAGE_CHECKBOX -> {
if (result!!.toBoolean()) { if (result!!.toBoolean()) {
webview.postDelayed({ webview.postDelayed(
getAudioButtonLocation(loopId) {
}, 250) getAudioButtonLocation(loopId)
},
250
)
} else { } else {
webview.postDelayed({ webview.postDelayed(
doStageCheckbox(loopId) {
}, 250) doStageCheckbox(loopId)
},
250
)
} }
} }
STAGE_GET_AUDIO_BTN_LOCATION -> { STAGE_GET_AUDIO_BTN_LOCATION -> {
@ -216,31 +227,43 @@ class BrowserActionActivity : AppCompatActivity() {
doStageDownloadAudio(loopId) doStageDownloadAudio(loopId)
} }
} else { } else {
webview.postDelayed({ webview.postDelayed(
getAudioButtonLocation(loopId) {
}, 250) getAudioButtonLocation(loopId)
},
250
)
} }
} }
STAGE_DOWNLOAD_AUDIO -> { STAGE_DOWNLOAD_AUDIO -> {
if (result != null) { if (result != null) {
Timber.d("Got audio URL: $result") Timber.d("Got audio URL: $result")
performRecognize(result) performRecognize(result)
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.subscribe({ .subscribe(
{
Timber.d("Got audio transcript: $it") Timber.d("Got audio transcript: $it")
webview.post { webview.post {
typeResult(loopId, it!! typeResult(
loopId,
it!!
.replace(TRANSCRIPT_CLEANER_REGEX, "") .replace(TRANSCRIPT_CLEANER_REGEX, "")
.replace(SPACE_DEDUPE_REGEX, " ") .replace(SPACE_DEDUPE_REGEX, " ")
.trim()) .trim()
)
} }
}, { },
{
captchaSolveFail() captchaSolveFail()
}) }
)
} else { } else {
webview.postDelayed({ webview.postDelayed(
doStageDownloadAudio(loopId) {
}, 250) doStageDownloadAudio(loopId)
},
250
)
} }
} }
STAGE_TYPE_RESULT -> { STAGE_TYPE_RESULT -> {
@ -256,27 +279,37 @@ class BrowserActionActivity : AppCompatActivity() {
fun performRecognize(url: String): Single<String> { fun performRecognize(url: String): Single<String> {
return credentialsObservable.flatMap { token -> return credentialsObservable.flatMap { token ->
httpClient.newCall(Request.Builder() httpClient.newCall(
Request.Builder()
.url(url) .url(url)
.build()).asObservableSuccess().map { .build()
).asObservableSuccess().map {
token to it token to it
} }
}.flatMap { (token, response) -> }.flatMap { (token, response) ->
val audioFile = response.body!!.bytes() val audioFile = response.body!!.bytes()
httpClient.newCall(Request.Builder() httpClient.newCall(
.url("https://stream.watsonplatform.net/speech-to-text/api/v1/recognize".toHttpUrlOrNull()!! Request.Builder()
.url(
"https://stream.watsonplatform.net/speech-to-text/api/v1/recognize".toHttpUrlOrNull()!!
.newBuilder() .newBuilder()
.addQueryParameter("watson-token", token) .addQueryParameter("watson-token", token)
.build()) .build()
.post(MultipartBody.Builder() )
.post(
MultipartBody.Builder()
.setType(MultipartBody.FORM) .setType(MultipartBody.FORM)
.addFormDataPart("jsonDescription", RECOGNIZE_JSON) .addFormDataPart("jsonDescription", RECOGNIZE_JSON)
.addFormDataPart("audio.mp3", .addFormDataPart(
"audio.mp3", "audio.mp3",
RequestBody.create("audio/mp3".toMediaTypeOrNull(), audioFile)) "audio.mp3",
.build()) RequestBody.create("audio/mp3".toMediaTypeOrNull(), audioFile)
.build()).asObservableSuccess() )
.build()
)
.build()
).asObservableSuccess()
}.map { response -> }.map { response ->
JsonParser.parseString(response.body!!.string())["results"][0]["alternatives"][0]["transcript"].string.trim() JsonParser.parseString(response.body!!.string())["results"][0]["alternatives"][0]["transcript"].string.trim()
}.toSingle() }.toSingle()
@ -285,7 +318,8 @@ class BrowserActionActivity : AppCompatActivity() {
fun doStageCheckbox(loopId: String) { fun doStageCheckbox(loopId: String) {
if (loopId != currentLoopId) return if (loopId != currentLoopId) return
webview.evaluateJavascript(""" webview.evaluateJavascript(
"""
(function() { (function() {
$CROSS_WINDOW_SCRIPT_OUTER $CROSS_WINDOW_SCRIPT_OUTER
@ -307,11 +341,14 @@ class BrowserActionActivity : AppCompatActivity() {
exh.callback("false", '$loopId', $STAGE_CHECKBOX); exh.callback("false", '$loopId', $STAGE_CHECKBOX);
} }
})(); })();
""".trimIndent().replace("\n", ""), null) """.trimIndent().replace("\n", ""),
null
)
} }
fun getAudioButtonLocation(loopId: String) { fun getAudioButtonLocation(loopId: String) {
webview.evaluateJavascript(""" webview.evaluateJavascript(
"""
(function() { (function() {
$CROSS_WINDOW_SCRIPT_OUTER $CROSS_WINDOW_SCRIPT_OUTER
@ -339,11 +376,14 @@ class BrowserActionActivity : AppCompatActivity() {
exh.callback(null, '$loopId', $STAGE_GET_AUDIO_BTN_LOCATION); exh.callback(null, '$loopId', $STAGE_GET_AUDIO_BTN_LOCATION);
} }
})(); })();
""".trimIndent().replace("\n", ""), null) """.trimIndent().replace("\n", ""),
null
)
} }
fun doStageDownloadAudio(loopId: String) { fun doStageDownloadAudio(loopId: String) {
webview.evaluateJavascript(""" webview.evaluateJavascript(
"""
(function() { (function() {
$CROSS_WINDOW_SCRIPT_OUTER $CROSS_WINDOW_SCRIPT_OUTER
@ -364,11 +404,14 @@ class BrowserActionActivity : AppCompatActivity() {
exh.callback(null, '$loopId', $STAGE_DOWNLOAD_AUDIO); exh.callback(null, '$loopId', $STAGE_DOWNLOAD_AUDIO);
} }
})(); })();
""".trimIndent().replace("\n", ""), null) """.trimIndent().replace("\n", ""),
null
)
} }
fun typeResult(loopId: String, result: String) { fun typeResult(loopId: String, result: String) {
webview.evaluateJavascript(""" webview.evaluateJavascript(
"""
(function() { (function() {
$CROSS_WINDOW_SCRIPT_OUTER $CROSS_WINDOW_SCRIPT_OUTER
@ -392,7 +435,9 @@ class BrowserActionActivity : AppCompatActivity() {
exh.callback("false", '$loopId', $STAGE_TYPE_RESULT); exh.callback("false", '$loopId', $STAGE_TYPE_RESULT);
} }
})(); })();
""".trimIndent().replace("\n", ""), null) """.trimIndent().replace("\n", ""),
null
)
} }
fun beginSolveLoop() { fun beginSolveLoop() {
@ -419,12 +464,16 @@ class BrowserActionActivity : AppCompatActivity() {
} else { } else {
val savedStrictValidationStartTime = strictValidationStartTime val savedStrictValidationStartTime = strictValidationStartTime
if (savedStrictValidationStartTime != null && if (savedStrictValidationStartTime != null &&
System.currentTimeMillis() > savedStrictValidationStartTime) { System.currentTimeMillis() > savedStrictValidationStartTime
) {
captchaSolveFail() captchaSolveFail()
} else { } else {
webview.postDelayed({ webview.postDelayed(
runValidateCaptcha(loopId) {
}, 250) runValidateCaptcha(loopId)
},
250
)
} }
} }
} }
@ -432,7 +481,8 @@ class BrowserActionActivity : AppCompatActivity() {
fun runValidateCaptcha(loopId: String) { fun runValidateCaptcha(loopId: String) {
if (loopId != validateCurrentLoopId) return if (loopId != validateCurrentLoopId) return
webview.evaluateJavascript(""" webview.evaluateJavascript(
"""
(function() { (function() {
$CROSS_WINDOW_SCRIPT_OUTER $CROSS_WINDOW_SCRIPT_OUTER
@ -453,7 +503,9 @@ class BrowserActionActivity : AppCompatActivity() {
exh.validateCaptchaCallback(false, '$loopId'); exh.validateCaptchaCallback(false, '$loopId');
} }
})(); })();
""".trimIndent().replace("\n", ""), null) """.trimIndent().replace("\n", ""),
null
)
} }
fun beginValidateCaptchaLoop() { fun beginValidateCaptchaLoop() {
@ -502,7 +554,8 @@ class BrowserActionActivity : AppCompatActivity() {
const val STAGE_DOWNLOAD_AUDIO = 2 const val STAGE_DOWNLOAD_AUDIO = 2
const val STAGE_TYPE_RESULT = 3 const val STAGE_TYPE_RESULT = 3
val CROSS_WINDOW_SCRIPT_OUTER = """ val CROSS_WINDOW_SCRIPT_OUTER =
"""
function cwmExec(element, code, cb) { function cwmExec(element, code, cb) {
console.log(">>> [CWM-Outer] Running: " + code); console.log(">>> [CWM-Outer] Running: " + code);
let runId = Math.random(); let runId = Math.random();
@ -523,9 +576,10 @@ class BrowserActionActivity : AppCompatActivity() {
let runRequest = { id: runId, code: code }; let runRequest = { id: runId, code: code };
element.contentWindow.postMessage("exh-" + JSON.stringify(runRequest), "*"); element.contentWindow.postMessage("exh-" + JSON.stringify(runRequest), "*");
} }
""".trimIndent().replace("\n", "") """.trimIndent().replace("\n", "")
val CROSS_WINDOW_SCRIPT_INNER = """ val CROSS_WINDOW_SCRIPT_INNER =
"""
window.addEventListener('message', function(event) { window.addEventListener('message', function(event) {
if(typeof event.data === "string" && event.data.startsWith("exh-")) { if(typeof event.data === "string" && event.data.startsWith("exh-")) {
let request = JSON.parse(event.data.substring(4)); let request = JSON.parse(event.data.substring(4));
@ -538,9 +592,10 @@ class BrowserActionActivity : AppCompatActivity() {
}, false); }, false);
console.log(">>> [CWM-Inner] Loaded!"); console.log(">>> [CWM-Inner] Loaded!");
alert("exh-"); alert("exh-");
""".trimIndent() """.trimIndent()
val SOLVE_UI_SCRIPT_SHOW = """ val SOLVE_UI_SCRIPT_SHOW =
"""
(function() { (function() {
let exh_overlay = document.createElement("div"); let exh_overlay = document.createElement("div");
exh_overlay.id = "exh_overlay"; exh_overlay.id = "exh_overlay";
@ -568,18 +623,20 @@ class BrowserActionActivity : AppCompatActivity() {
exh_otext.textContent = "Solving captcha..." exh_otext.textContent = "Solving captcha..."
document.body.appendChild(exh_otext); document.body.appendChild(exh_otext);
})(); })();
""".trimIndent() """.trimIndent()
val SOLVE_UI_SCRIPT_HIDE = """ val SOLVE_UI_SCRIPT_HIDE =
"""
(function() { (function() {
let exh_overlay = document.getElementById("exh_overlay"); let exh_overlay = document.getElementById("exh_overlay");
let exh_otext = document.getElementById("exh_otext"); let exh_otext = document.getElementById("exh_otext");
if(exh_overlay != null) exh_overlay.remove(); if(exh_overlay != null) exh_overlay.remove();
if(exh_otext != null) exh_otext.remove(); if(exh_otext != null) exh_otext.remove();
})(); })();
""".trimIndent() """.trimIndent()
val RECOGNIZE_JSON = """ val RECOGNIZE_JSON =
"""
{ {
"part_content_type": "audio/mp3", "part_content_type": "audio/mp3",
"keywords": [], "keywords": [],
@ -596,15 +653,15 @@ class BrowserActionActivity : AppCompatActivity() {
"customGrammarWords": [], "customGrammarWords": [],
"action": "recognize" "action": "recognize"
} }
""".trimIndent() """.trimIndent()
val TRANSCRIPT_CLEANER_REGEX = Regex("[^0-9a-zA-Z_ -]") val TRANSCRIPT_CLEANER_REGEX = Regex("[^0-9a-zA-Z_ -]")
val SPACE_DEDUPE_REGEX = Regex(" +") val SPACE_DEDUPE_REGEX = Regex(" +")
private fun baseIntent(context: Context) = private fun baseIntent(context: Context) =
Intent(context, BrowserActionActivity::class.java).apply { Intent(context, BrowserActionActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
fun launchCaptcha( fun launchCaptcha(
context: Context, context: Context,
@ -689,8 +746,9 @@ class BrowserActionActivity : AppCompatActivity() {
} }
} }
class NoopActionCompletionVerifier(private val source: HttpSource) : DelegatedHttpSource(source), class NoopActionCompletionVerifier(private val source: HttpSource) :
ActionCompletionVerifier { DelegatedHttpSource(source),
ActionCompletionVerifier {
override val versionId get() = source.versionId override val versionId get() = source.versionId
override val lang: String get() = source.lang override val lang: String get() = source.lang

View File

@ -37,43 +37,43 @@ open class HeadersInjectingWebViewClient(
companion object { companion object {
private val FALLBACK_REASON_PHRASES = mapOf( private val FALLBACK_REASON_PHRASES = mapOf(
100 to "Continue", 100 to "Continue",
101 to "Switching Protocols", 101 to "Switching Protocols",
200 to "OK", 200 to "OK",
201 to "Created", 201 to "Created",
202 to "Accepted", 202 to "Accepted",
203 to "Non-Authoritative Information", 203 to "Non-Authoritative Information",
204 to "No Content", 204 to "No Content",
205 to "Reset Content", 205 to "Reset Content",
206 to "Partial Content", 206 to "Partial Content",
300 to "Multiple Choices", 300 to "Multiple Choices",
301 to "Moved Permanently", 301 to "Moved Permanently",
302 to "Moved Temporarily", 302 to "Moved Temporarily",
303 to "See Other", 303 to "See Other",
304 to "Not Modified", 304 to "Not Modified",
305 to "Use Proxy", 305 to "Use Proxy",
400 to "Bad Request", 400 to "Bad Request",
401 to "Unauthorized", 401 to "Unauthorized",
402 to "Payment Required", 402 to "Payment Required",
403 to "Forbidden", 403 to "Forbidden",
404 to "Not Found", 404 to "Not Found",
405 to "Method Not Allowed", 405 to "Method Not Allowed",
406 to "Not Acceptable", 406 to "Not Acceptable",
407 to "Proxy Authentication Required", 407 to "Proxy Authentication Required",
408 to "Request Time-out", 408 to "Request Time-out",
409 to "Conflict", 409 to "Conflict",
410 to "Gone", 410 to "Gone",
411 to "Length Required", 411 to "Length Required",
412 to "Precondition Failed", 412 to "Precondition Failed",
413 to "Request Entity Too Large", 413 to "Request Entity Too Large",
414 to "Request-URI Too Large", 414 to "Request-URI Too Large",
415 to "Unsupported Media Type", 415 to "Unsupported Media Type",
500 to "Internal Server Error", 500 to "Internal Server Error",
501 to "Not Implemented", 501 to "Not Implemented",
502 to "Bad Gateway", 502 to "Bad Gateway",
503 to "Service Unavailable", 503 to "Service Unavailable",
504 to "Gateway Time-out", 504 to "Gateway Time-out",
505 to "HTTP Version not supported" 505 to "HTTP Version not supported"
) )
} }
} }

View File

@ -5,8 +5,8 @@ import okhttp3.Request
fun WebResourceRequest.toOkHttpRequest(): Request { fun WebResourceRequest.toOkHttpRequest(): Request {
val request = Request.Builder() val request = Request.Builder()
.url(url.toString()) .url(url.toString())
.method(method, null) .method(method, null)
requestHeaders.entries.forEach { (t, u) -> requestHeaders.entries.forEach { (t, u) ->
request.addHeader(t, u) request.addHeader(t, u)

View File

@ -54,33 +54,35 @@ class InterceptActivity : BaseRxActivity<EhActivityInterceptBinding, InterceptAc
super.onStart() super.onStart()
statusSubscription?.unsubscribe() statusSubscription?.unsubscribe()
statusSubscription = presenter.status statusSubscription = presenter.status
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
when (it) { when (it) {
is InterceptResult.Success -> { is InterceptResult.Success -> {
binding.interceptProgress.gone() binding.interceptProgress.gone()
binding.interceptStatus.text = "Launching app..." binding.interceptStatus.text = "Launching app..."
onBackPressed() onBackPressed()
startActivity(Intent(this, MainActivity::class.java) startActivity(
.setAction(MainActivity.SHORTCUT_MANGA) Intent(this, MainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) .setAction(MainActivity.SHORTCUT_MANGA)
.putExtra(MangaController.MANGA_EXTRA, it.mangaId)) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
} .putExtra(MangaController.MANGA_EXTRA, it.mangaId)
is InterceptResult.Failure -> { )
binding.interceptProgress.gone() }
binding.interceptStatus.text = "Error: ${it.reason}" is InterceptResult.Failure -> {
MaterialDialog(this) binding.interceptProgress.gone()
.title(text = "Error") binding.interceptStatus.text = "Error: ${it.reason}"
.message(text = "Could not open this gallery:\n\n${it.reason}") MaterialDialog(this)
.cancelable(true) .title(text = "Error")
.cancelOnTouchOutside(true) .message(text = "Could not open this gallery:\n\n${it.reason}")
.positiveButton(android.R.string.ok) .cancelable(true)
.onCancel { onBackPressed() } .cancelOnTouchOutside(true)
.onDismiss { onBackPressed() } .positiveButton(android.R.string.ok)
.show() .onCancel { onBackPressed() }
} .onDismiss { onBackPressed() }
.show()
} }
} }
}
} }
override fun onStop() { override fun onStop() {

View File

@ -21,12 +21,14 @@ class InterceptActivityPresenter : BasePresenter<InterceptActivity>() {
thread { thread {
val result = galleryAdder.addGallery(gallery) val result = galleryAdder.addGallery(gallery)
status.onNext(when (result) { status.onNext(
is GalleryAddEvent.Success -> result.manga.id?.let { when (result) {
InterceptResult.Success(it) is GalleryAddEvent.Success -> result.manga.id?.let {
} ?: InterceptResult.Failure("Manga ID is null!") InterceptResult.Success(it)
is GalleryAddEvent.Fail -> InterceptResult.Failure(result.logMessage) } ?: InterceptResult.Failure("Manga ID is null!")
}) is GalleryAddEvent.Fail -> InterceptResult.Failure(result.logMessage)
}
)
} }
} }
} }

View File

@ -25,18 +25,18 @@ import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
SwitchPreferenceCompat(context, attrs) { SwitchPreferenceCompat(context, attrs) {
val prefs: PreferencesHelper by injectLazy() val prefs: PreferencesHelper by injectLazy()
val fingerprintSupported val fingerprintSupported
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
Reprint.isHardwarePresent() && Reprint.isHardwarePresent() &&
Reprint.hasFingerprintRegistered() Reprint.hasFingerprintRegistered()
val useFingerprint val useFingerprint
get() = fingerprintSupported && get() = fingerprintSupported &&
prefs.eh_lockUseFingerprint().getOrDefault() prefs.eh_lockUseFingerprint().getOrDefault()
@SuppressLint("NewApi") @SuppressLint("NewApi")
override fun onAttached() { override fun onAttached() {
@ -44,29 +44,32 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
if (fingerprintSupported) { if (fingerprintSupported) {
updateSummary() updateSummary()
onChange { onChange {
if (it as Boolean) if (it as Boolean) {
tryChange() tryChange()
else } else {
prefs.eh_lockUseFingerprint().set(false) prefs.eh_lockUseFingerprint().set(false)
}
!it !it
} }
} else { } else {
title = "Fingerprint unsupported" title = "Fingerprint unsupported"
shouldDisableView = true shouldDisableView = true
summary = if (!Reprint.hasFingerprintRegistered()) summary = if (!Reprint.hasFingerprintRegistered()) {
"No fingerprints enrolled!" "No fingerprints enrolled!"
else } else {
"Fingerprint unlock is unsupported on this device!" "Fingerprint unlock is unsupported on this device!"
}
onChange { false } onChange { false }
} }
} }
private fun updateSummary() { private fun updateSummary() {
isChecked = useFingerprint isChecked = useFingerprint
title = if (isChecked) title = if (isChecked) {
"Fingerprint enabled" "Fingerprint enabled"
else } else {
"Fingerprint disabled" "Fingerprint disabled"
}
} }
@TargetApi(Build.VERSION_CODES.M) @TargetApi(Build.VERSION_CODES.M)
@ -74,9 +77,11 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
val statusTextView = TextView(context).apply { val statusTextView = TextView(context).apply {
text = "Please touch the fingerprint sensor" text = "Please touch the fingerprint sensor"
val size = ViewGroup.LayoutParams.WRAP_CONTENT val size = ViewGroup.LayoutParams.WRAP_CONTENT
layoutParams = (layoutParams ?: ViewGroup.LayoutParams( layoutParams = (
layoutParams ?: ViewGroup.LayoutParams(
size, size size, size
)).apply { )
).apply {
width = size width = size
height = size height = size
setPadding(0, 0, dpToPx(context, 8), 0) setPadding(0, 0, dpToPx(context, 8), 0)
@ -84,9 +89,11 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
} }
val iconView = SwirlView(context).apply { val iconView = SwirlView(context).apply {
val size = dpToPx(context, 30) val size = dpToPx(context, 30)
layoutParams = (layoutParams ?: ViewGroup.LayoutParams( layoutParams = (
layoutParams ?: ViewGroup.LayoutParams(
size, size size, size
)).apply { )
).apply {
width = size width = size
height = size height = size
} }
@ -96,9 +103,11 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
orientation = LinearLayoutCompat.HORIZONTAL orientation = LinearLayoutCompat.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL gravity = Gravity.CENTER_VERTICAL
val size = LinearLayoutCompat.LayoutParams.WRAP_CONTENT val size = LinearLayoutCompat.LayoutParams.WRAP_CONTENT
layoutParams = (layoutParams ?: LinearLayoutCompat.LayoutParams( layoutParams = (
layoutParams ?: LinearLayoutCompat.LayoutParams(
size, size size, size
)).apply { )
).apply {
width = size width = size
height = size height = size
val pSize = dpToPx(context, 24) val pSize = dpToPx(context, 24)
@ -109,39 +118,39 @@ class FingerLockPreference @JvmOverloads constructor(context: Context, attrs: At
addView(iconView) addView(iconView)
} }
val dialog = MaterialDialog(context) val dialog = MaterialDialog(context)
.title(text = "Fingerprint verification") .title(text = "Fingerprint verification")
.customView(view = linearLayout) .customView(view = linearLayout)
.negativeButton(R.string.action_cancel) .negativeButton(R.string.action_cancel)
.cancelable(true) .cancelable(true)
.cancelOnTouchOutside(true) .cancelOnTouchOutside(true)
dialog.show() dialog.show()
iconView.setState(SwirlView.State.ON) iconView.setState(SwirlView.State.ON)
val subscription = RxReprint.authenticate() val subscription = RxReprint.authenticate()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { result -> .subscribe { result ->
when (result.status) { when (result.status) {
AuthenticationResult.Status.SUCCESS -> { AuthenticationResult.Status.SUCCESS -> {
iconView.setState(SwirlView.State.ON) iconView.setState(SwirlView.State.ON)
prefs.eh_lockUseFingerprint().set(true) prefs.eh_lockUseFingerprint().set(true)
dialog.dismiss() dialog.dismiss()
updateSummary() updateSummary()
} }
AuthenticationResult.Status.NONFATAL_FAILURE -> { AuthenticationResult.Status.NONFATAL_FAILURE -> {
iconView.setState(SwirlView.State.ERROR) iconView.setState(SwirlView.State.ERROR)
statusTextView.text = result.errorMessage statusTextView.text = result.errorMessage
} }
AuthenticationResult.Status.FATAL_FAILURE, null -> { AuthenticationResult.Status.FATAL_FAILURE, null -> {
MaterialDialog(context) MaterialDialog(context)
.title(text = "Fingerprint verification failed!") .title(text = "Fingerprint verification failed!")
.message(text = result.errorMessage) .message(text = result.errorMessage)
.positiveButton(android.R.string.ok) .positiveButton(android.R.string.ok)
.cancelable(true) .cancelable(true)
.cancelOnTouchOutside(false) .cancelOnTouchOutside(false)
.show() .show()
dialog.dismiss() dialog.dismiss()
}
} }
} }
}
dialog.setOnDismissListener { dialog.setOnDismissListener {
subscription.unsubscribe() subscription.unsubscribe()
} }

View File

@ -20,19 +20,21 @@ object LockActivityDelegate {
private val uiScope = CoroutineScope(Dispatchers.Main) private val uiScope = CoroutineScope(Dispatchers.Main)
fun doLock(router: Router, animate: Boolean = false) { fun doLock(router: Router, animate: Boolean = false) {
router.pushController(RouterTransaction.with(LockController()) router.pushController(
.popChangeHandler(LockChangeHandler(animate))) RouterTransaction.with(LockController())
.popChangeHandler(LockChangeHandler(animate))
)
} }
fun onCreate(activity: FragmentActivity) { fun onCreate(activity: FragmentActivity) {
preferences.secureScreen().asFlow() preferences.secureScreen().asFlow()
.onEach { .onEach {
if (it) { if (it) {
activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
} else { } else {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
} }
}
.launchIn(uiScope) .launchIn(uiScope)
} }

View File

@ -35,5 +35,5 @@ class LockChangeHandler : AnimatorChangeHandler {
override fun resetFromView(from: View) {} override fun resetFromView(from: View) {}
override fun copy(): ControllerChangeHandler = override fun copy(): ControllerChangeHandler =
LockChangeHandler(animationDuration, removesFromViewOnPush()) LockChangeHandler(animationDuration, removesFromViewOnPush())
} }

View File

@ -53,12 +53,12 @@ class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
closeLock() closeLock()
} else { } else {
MaterialDialog(context) MaterialDialog(context)
.title(text = "PIN code incorrect") .title(text = "PIN code incorrect")
.message(text = "The PIN code you entered is incorrect. Please try again.") .message(text = "The PIN code you entered is incorrect. Please try again.")
.cancelable(true) .cancelable(true)
.cancelOnTouchOutside(true) .cancelOnTouchOutside(true)
.positiveButton(android.R.string.ok) .positiveButton(android.R.string.ok)
.show() .show()
binding.pinLockView.resetPinLockView() binding.pinLockView.resetPinLockView()
} }
} }
@ -79,9 +79,11 @@ class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
binding.swirlContainer.removeAllViews() binding.swirlContainer.removeAllViews()
val icon = SwirlView(context).apply { val icon = SwirlView(context).apply {
val size = dpToPx(context, 60) val size = dpToPx(context, 60)
layoutParams = (layoutParams ?: ViewGroup.LayoutParams( layoutParams = (
layoutParams ?: ViewGroup.LayoutParams(
size, size size, size
)).apply { )
).apply {
width = size width = size
height = size height = size
@ -92,29 +94,30 @@ class LockController : NucleusController<ActivityLockBinding, LockPresenter>() {
setBackgroundColor(lockColor) setBackgroundColor(lockColor)
val bgColor = resolvColor(android.R.attr.colorBackground) val bgColor = resolvColor(android.R.attr.colorBackground)
// Disable elevation if lock color is same as background color // Disable elevation if lock color is same as background color
if (lockColor == bgColor) if (lockColor == bgColor) {
this@with.swirl_container.cardElevation = 0f this@with.swirl_container.cardElevation = 0f
}
setState(SwirlView.State.OFF, true) setState(SwirlView.State.OFF, true)
} }
binding.swirlContainer.addView(icon) binding.swirlContainer.addView(icon)
icon.setState(SwirlView.State.ON) icon.setState(SwirlView.State.ON)
RxReprint.authenticate() RxReprint.authenticate()
.subscribeUntilDetach { .subscribeUntilDetach {
when (it.status) { when (it.status) {
AuthenticationResult.Status.SUCCESS -> closeLock() AuthenticationResult.Status.SUCCESS -> closeLock()
AuthenticationResult.Status.NONFATAL_FAILURE -> icon.setState(SwirlView.State.ERROR) AuthenticationResult.Status.NONFATAL_FAILURE -> icon.setState(SwirlView.State.ERROR)
AuthenticationResult.Status.FATAL_FAILURE, null -> { AuthenticationResult.Status.FATAL_FAILURE, null -> {
MaterialDialog(context) MaterialDialog(context)
.title(text = "Fingerprint error!") .title(text = "Fingerprint error!")
.message(text = it.errorMessage) .message(text = it.errorMessage)
.cancelable(false) .cancelable(false)
.cancelOnTouchOutside(false) .cancelOnTouchOutside(false)
.positiveButton(android.R.string.ok) .positiveButton(android.R.string.ok)
.show() .show()
icon.setState(SwirlView.State.OFF) icon.setState(SwirlView.State.OFF)
}
} }
} }
}
} else { } else {
binding.swirlContainer.visibility = View.GONE binding.swirlContainer.visibility = View.GONE
} }

View File

@ -17,7 +17,7 @@ import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class LockPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
SwitchPreferenceCompat(context, attrs) { SwitchPreferenceCompat(context, attrs) {
private val secureRandom by lazy { SecureRandom() } private val secureRandom by lazy { SecureRandom() }
@ -46,28 +46,28 @@ class LockPreference @JvmOverloads constructor(context: Context, attrs: Attribut
fun tryChange() { fun tryChange() {
if (!notifyLockSecurity(context)) { if (!notifyLockSecurity(context)) {
MaterialDialog(context) MaterialDialog(context)
.title(text = "Lock application") .title(text = "Lock application")
.message(text = "Enter a pin to lock the application. Enter nothing to disable the pin lock.") .message(text = "Enter a pin to lock the application. Enter nothing to disable the pin lock.")
// .inputRangeRes(0, 10, R.color.material_red_500) // .inputRangeRes(0, 10, R.color.material_red_500)
// .inputType(InputType.TYPE_CLASS_NUMBER) // .inputType(InputType.TYPE_CLASS_NUMBER)
.input(maxLength = 10, inputType = InputType.TYPE_CLASS_NUMBER, allowEmpty = true) { _, c -> .input(maxLength = 10, inputType = InputType.TYPE_CLASS_NUMBER, allowEmpty = true) { _, c ->
val progressDialog = MaterialDialog(context) val progressDialog = MaterialDialog(context)
.title(text = "Saving password") .title(text = "Saving password")
.cancelable(false) .cancelable(false)
progressDialog.show() progressDialog.show()
Observable.fromCallable { Observable.fromCallable {
savePassword(c.toString()) savePassword(c.toString())
}.subscribeOn(Schedulers.computation()) }.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
progressDialog.dismiss() progressDialog.dismiss()
updateSummary() updateSummary()
} }
} }
.negativeButton(R.string.action_cancel) .negativeButton(R.string.action_cancel)
.cancelable(true) .cancelable(true)
.cancelOnTouchOutside(true) .cancelOnTouchOutside(true)
.show() .show()
} }
} }

View File

@ -12,7 +12,7 @@ class LockPresenter : BasePresenter<LockController>() {
val useFingerprint val useFingerprint
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
Reprint.isHardwarePresent() && Reprint.isHardwarePresent() &&
Reprint.hasFingerprintRegistered() && Reprint.hasFingerprintRegistered() &&
prefs.eh_lockUseFingerprint().getOrDefault() prefs.eh_lockUseFingerprint().getOrDefault()
} }

View File

@ -39,8 +39,8 @@ fun sha512(passwordToHash: String, salt: String): String {
*/ */
fun lockEnabled(prefs: PreferencesHelper = Injekt.get()) = fun lockEnabled(prefs: PreferencesHelper = Injekt.get()) =
prefs.eh_lockHash().get() != null && prefs.eh_lockHash().get() != null &&
prefs.eh_lockSalt().get() != null && prefs.eh_lockSalt().get() != null &&
prefs.eh_lockLength().getOrDefault() != -1 prefs.eh_lockLength().getOrDefault() != -1
/** /**
* Check if the lock will function properly * Check if the lock will function properly
@ -53,30 +53,35 @@ fun notifyLockSecurity(
): Boolean { ): Boolean {
return false return false
if (!prefs.eh_lockManually().getOrDefault() && if (!prefs.eh_lockManually().getOrDefault() &&
!hasAccessToUsageStats(context)) { !hasAccessToUsageStats(context)
) {
MaterialDialog(context) MaterialDialog(context)
.title(text = "Permission required") .title(text = "Permission required")
.message(text = "${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " + .message(
"This is required for the application lock to function properly. " + text = "${context.getString(R.string.app_name)} requires the usage stats permission to detect when you leave the app. " +
"Press OK to grant this permission now.") "This is required for the application lock to function properly. " +
.negativeButton(R.string.action_cancel) "Press OK to grant this permission now."
.positiveButton(android.R.string.ok) { )
try { .negativeButton(R.string.action_cancel)
context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)) .positiveButton(android.R.string.ok) {
} catch (e: ActivityNotFoundException) { try {
XLog.e("Device does not support USAGE_ACCESS_SETTINGS shortcut!") context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
MaterialDialog(context) } catch (e: ActivityNotFoundException) {
.title(text = "Grant permission manually") XLog.e("Device does not support USAGE_ACCESS_SETTINGS shortcut!")
.message(text = "Failed to launch the window used to grant the usage stats permission. " + MaterialDialog(context)
"You can still grant this permission manually: go to your phone's settings and search for 'usage access'.") .title(text = "Grant permission manually")
.positiveButton(android.R.string.ok) { it.dismiss() } .message(
.cancelable(true) text = "Failed to launch the window used to grant the usage stats permission. " +
.cancelOnTouchOutside(false) "You can still grant this permission manually: go to your phone's settings and search for 'usage access'."
.show() )
} .positiveButton(android.R.string.ok) { it.dismiss() }
.cancelable(true)
.cancelOnTouchOutside(false)
.show()
} }
.cancelable(false) }
.show() .cancelable(false)
.show()
return true return true
} else { } else {
return false return false

View File

@ -97,10 +97,11 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
val parsedUrl = Uri.parse(url) val parsedUrl = Uri.parse(url)
if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) { if (parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
// Hide distracting content // Hide distracting content
if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) if (!parsedUrl.queryParameterNames.contains(PARAM_SKIP_INJECT)) {
view.evaluateJavascript(HIDE_JS, null) view.evaluateJavascript(HIDE_JS, null)
}
// Check login result // Check login result
if (parsedUrl.getQueryParameter("code")?.toInt() != 0) { if (parsedUrl.getQueryParameter("code")?.toInt() != 0) {
if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/") if (checkLoginCookies(url)) view.loadUrl("https://exhentai.org/")
} }
@ -128,9 +129,11 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
fun checkLoginCookies(url: String): Boolean { fun checkLoginCookies(url: String): Boolean {
getCookies(url)?.let { parsed -> getCookies(url)?.let { parsed ->
return parsed.filter { return parsed.filter {
(it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true) || (
it.name.equals(PASS_HASH_COOKIE, ignoreCase = true)) && it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true) ||
it.value.isNotBlank() it.name.equals(PASS_HASH_COOKIE, ignoreCase = true)
) &&
it.value.isNotBlank()
}.count() >= 2 }.count() >= 2
} }
return false return false
@ -168,11 +171,11 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
} }
fun getCookies(url: String): List<HttpCookie>? = fun getCookies(url: String): List<HttpCookie>? =
CookieManager.getInstance().getCookie(url)?.let { CookieManager.getInstance().getCookie(url)?.let {
it.split("; ").flatMap { it.split("; ").flatMap {
HttpCookie.parse(it) HttpCookie.parse(it)
}
} }
}
companion object { companion object {
const val PARAM_SKIP_INJECT = "TEH_SKIP_INJECT" const val PARAM_SKIP_INJECT = "TEH_SKIP_INJECT"
@ -181,7 +184,8 @@ class LoginController : NucleusController<EhActivityLoginBinding, LoginPresenter
const val PASS_HASH_COOKIE = "ipb_pass_hash" const val PASS_HASH_COOKIE = "ipb_pass_hash"
const val IGNEOUS_COOKIE = "igneous" const val IGNEOUS_COOKIE = "igneous"
const val HIDE_JS = """ const val HIDE_JS =
"""
javascript:(function () { javascript:(function () {
document.getElementsByTagName('body')[0].style.visibility = 'hidden'; document.getElementsByTagName('body')[0].style.visibility = 'hidden';
document.getElementsByName('submit')[0].style.visibility = 'visible'; document.getElementsByName('submit')[0].style.visibility = 'visible';

View File

@ -16,7 +16,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SmartSearchPresenter(private val source: CatalogueSource?, private val config: SourceController.SmartSearchConfig?) : class SmartSearchPresenter(private val source: CatalogueSource?, private val config: SourceController.SmartSearchConfig?) :
BasePresenter<SmartSearchController>(), CoroutineScope { BasePresenter<SmartSearchController>(), CoroutineScope {
override val coroutineContext = Job() + Dispatchers.Main override val coroutineContext = Job() + Dispatchers.Main

View File

@ -14,16 +14,16 @@ import java.util.Date
inline fun <reified E : RealmModel> RealmQuery<out E>.beginLog( inline fun <reified E : RealmModel> RealmQuery<out E>.beginLog(
clazz: Class<out E>? = clazz: Class<out E>? =
E::class.java E::class.java
): LoggingRealmQuery<out E> = ): LoggingRealmQuery<out E> =
LoggingRealmQuery.fromQuery(this, clazz) LoggingRealmQuery.fromQuery(this, clazz)
class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) { class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
companion object { companion object {
fun <E : RealmModel> fromQuery(q: RealmQuery<out E>, clazz: Class<out E>?) = fun <E : RealmModel> fromQuery(q: RealmQuery<out E>, clazz: Class<out E>?) =
LoggingRealmQuery(q).apply { LoggingRealmQuery(q).apply {
log += "SELECT * FROM ${clazz?.name ?: "???"} WHERE" log += "SELECT * FROM ${clazz?.name ?: "???"} WHERE"
} }
} }
private val log = mutableListOf<String>() private val log = mutableListOf<String>()
@ -47,9 +47,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
} }
private fun appendEqualTo(fieldName: String, value: String, casing: Case? = null) { private fun appendEqualTo(fieldName: String, value: String, casing: Case? = null) {
log += sec("\"$fieldName\" == \"$value\"" + (casing?.let { log += sec(
" CASE ${casing.name}" "\"$fieldName\" == \"$value\"" + (
} ?: "")) casing?.let {
" CASE ${casing.name}"
} ?: ""
)
)
} }
fun equalTo(fieldName: String, value: String): RealmQuery<E> { fun equalTo(fieldName: String, value: String): RealmQuery<E> {
@ -108,11 +112,18 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
} }
fun appendIn(fieldName: String, values: Array<out Any?>, casing: Case? = null) { fun appendIn(fieldName: String, values: Array<out Any?>, casing: Case? = null) {
log += sec("[${values.joinToString(separator = ", ", transform = { log += sec(
"\"$it\"" "[${values.joinToString(
})}] IN \"$fieldName\"" + (casing?.let { separator = ", ",
" CASE ${casing.name}" transform = {
} ?: "")) "\"$it\""
}
)}] IN \"$fieldName\"" + (
casing?.let {
" CASE ${casing.name}"
} ?: ""
)
)
} }
fun `in`(fieldName: String, values: Array<String>): RealmQuery<E> { fun `in`(fieldName: String, values: Array<String>): RealmQuery<E> {
@ -166,9 +177,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
} }
private fun appendNotEqualTo(fieldName: String, value: Any?, casing: Case? = null) { private fun appendNotEqualTo(fieldName: String, value: Any?, casing: Case? = null) {
log += sec("\"$fieldName\" != \"$value\"" + (casing?.let { log += sec(
" CASE ${casing.name}" "\"$fieldName\" != \"$value\"" + (
} ?: "")) casing?.let {
" CASE ${casing.name}"
} ?: ""
)
)
} }
fun notEqualTo(fieldName: String, value: String): RealmQuery<E> { fun notEqualTo(fieldName: String, value: String): RealmQuery<E> {
@ -372,9 +387,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
} }
private fun appendContains(fieldName: String, value: Any?, casing: Case? = null) { private fun appendContains(fieldName: String, value: Any?, casing: Case? = null) {
log += sec("\"$fieldName\" CONTAINS \"$value\"" + (casing?.let { log += sec(
" CASE ${casing.name}" "\"$fieldName\" CONTAINS \"$value\"" + (
} ?: "")) casing?.let {
" CASE ${casing.name}"
} ?: ""
)
)
} }
fun contains(fieldName: String, value: String): RealmQuery<E> { fun contains(fieldName: String, value: String): RealmQuery<E> {
@ -388,9 +407,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
} }
private fun appendBeginsWith(fieldName: String, value: Any?, casing: Case? = null) { private fun appendBeginsWith(fieldName: String, value: Any?, casing: Case? = null) {
log += sec("\"$fieldName\" BEGINS WITH \"$value\"" + (casing?.let { log += sec(
" CASE ${casing.name}" "\"$fieldName\" BEGINS WITH \"$value\"" + (
} ?: "")) casing?.let {
" CASE ${casing.name}"
} ?: ""
)
)
} }
fun beginsWith(fieldName: String, value: String): RealmQuery<E> { fun beginsWith(fieldName: String, value: String): RealmQuery<E> {
@ -404,9 +427,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
} }
private fun appendEndsWith(fieldName: String, value: Any?, casing: Case? = null) { private fun appendEndsWith(fieldName: String, value: Any?, casing: Case? = null) {
log += sec("\"$fieldName\" ENDS WITH \"$value\"" + (casing?.let { log += sec(
" CASE ${casing.name}" "\"$fieldName\" ENDS WITH \"$value\"" + (
} ?: "")) casing?.let {
" CASE ${casing.name}"
} ?: ""
)
)
} }
fun endsWith(fieldName: String, value: String): RealmQuery<E> { fun endsWith(fieldName: String, value: String): RealmQuery<E> {
@ -420,9 +447,13 @@ class LoggingRealmQuery<E : RealmModel>(val query: RealmQuery<E>) {
} }
private fun appendLike(fieldName: String, value: Any?, casing: Case? = null) { private fun appendLike(fieldName: String, value: Any?, casing: Case? = null) {
log += sec("\"$fieldName\" LIKE \"$value\"" + (casing?.let { log += sec(
" CASE ${casing.name}" "\"$fieldName\" LIKE \"$value\"" + (
} ?: "")) casing?.let {
" CASE ${casing.name}"
} ?: ""
)
)
} }
fun like(fieldName: String, value: String): RealmQuery<E> { fun like(fieldName: String, value: String): RealmQuery<E> {

View File

@ -209,10 +209,14 @@ class NakedTrie<T> : MutableMap<String, T> {
override val entries: Set<Map.Entry<String, T>> override val entries: Set<Map.Entry<String, T>>
get() { get() {
val out = mutableSetOf<Map.Entry<String, T>>() val out = mutableSetOf<Map.Entry<String, T>>()
node.walk("", { k, v -> node.walk(
out.add(AbstractMap.SimpleImmutableEntry(k, v)) "",
true { k, v ->
}, leavesOnly) out.add(AbstractMap.SimpleImmutableEntry(k, v))
true
},
leavesOnly
)
return out return out
} }
/** /**
@ -221,10 +225,14 @@ class NakedTrie<T> : MutableMap<String, T> {
override val keys: Set<String> override val keys: Set<String>
get() { get() {
val out = mutableSetOf<String>() val out = mutableSetOf<String>()
node.walk("", { k, _ -> node.walk(
out.add(k) "",
true { k, _ ->
}, leavesOnly) out.add(k)
true
},
leavesOnly
)
return out return out
} }
@ -243,10 +251,14 @@ class NakedTrie<T> : MutableMap<String, T> {
override val values: Collection<T> override val values: Collection<T>
get() { get() {
val out = mutableSetOf<T>() val out = mutableSetOf<T>()
node.walk("", { _, v -> node.walk(
out.add(v) "",
true { _, v ->
}, leavesOnly) out.add(v)
true
},
leavesOnly
)
return out return out
} }
@ -264,10 +276,14 @@ class NakedTrie<T> : MutableMap<String, T> {
* Returns `true` if the map maps one or more keys to the specified [value]. * Returns `true` if the map maps one or more keys to the specified [value].
*/ */
override fun containsValue(value: T): Boolean { override fun containsValue(value: T): Boolean {
node.walk("", { _, v -> node.walk(
if (v == value) return true "",
true { _, v ->
}, leavesOnly) if (v == value) return true
true
},
leavesOnly
)
return false return false
} }
@ -315,32 +331,38 @@ class NakedTrie<T> : MutableMap<String, T> {
* Returns a [MutableSet] of all key/value pairs in this map. * Returns a [MutableSet] of all key/value pairs in this map.
*/ */
override val entries: MutableSet<MutableMap.MutableEntry<String, T>> override val entries: MutableSet<MutableMap.MutableEntry<String, T>>
get() = FakeMutableSet.fromSet(mutableSetOf<MutableMap.MutableEntry<String, T>>().apply { get() = FakeMutableSet.fromSet(
walk { k, v -> mutableSetOf<MutableMap.MutableEntry<String, T>>().apply {
this += FakeMutableEntry.fromPair(k, v) walk { k, v ->
true this += FakeMutableEntry.fromPair(k, v)
true
}
} }
}) )
/** /**
* Returns a [MutableSet] of all keys in this map. * Returns a [MutableSet] of all keys in this map.
*/ */
override val keys: MutableSet<String> override val keys: MutableSet<String>
get() = FakeMutableSet.fromSet(mutableSetOf<String>().apply { get() = FakeMutableSet.fromSet(
walk { k, _ -> mutableSetOf<String>().apply {
this += k walk { k, _ ->
true this += k
true
}
} }
}) )
/** /**
* Returns a [MutableCollection] of all values in this map. Note that this collection may contain duplicate values. * Returns a [MutableCollection] of all values in this map. Note that this collection may contain duplicate values.
*/ */
override val values: MutableCollection<T> override val values: MutableCollection<T>
get() = FakeMutableCollection.fromCollection(mutableListOf<T>().apply { get() = FakeMutableCollection.fromCollection(
walk { _, v -> mutableListOf<T>().apply {
this += v walk { _, v ->
true this += v
true
}
} }
}) )
} }

View File

@ -9,11 +9,12 @@ import org.jsoup.nodes.Document
fun Response.interceptAsHtml(block: (Document) -> Unit): Response { fun Response.interceptAsHtml(block: (Document) -> Unit): Response {
val body = body val body = body
if (body?.contentType()?.type == "text" && if (body?.contentType()?.type == "text" &&
body.contentType()?.subtype == "html") { body.contentType()?.subtype == "html"
) {
val bodyString = body.string() val bodyString = body.string()
val rebuiltResponse = newBuilder() val rebuiltResponse = newBuilder()
.body(ResponseBody.create(body.contentType(), bodyString)) .body(ResponseBody.create(body.contentType(), bodyString))
.build() .build()
try { try {
// Search for captcha // Search for captcha
val parsed = asJsoup(html = bodyString) val parsed = asJsoup(html = bodyString)

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