diff --git a/app/src/main/java/eu/kanade/data/source/EHentaiPagingSource.kt b/app/src/main/java/eu/kanade/data/source/EHentaiPagingSource.kt index 999b5bd8c..92ef20350 100644 --- a/app/src/main/java/eu/kanade/data/source/EHentaiPagingSource.kt +++ b/app/src/main/java/eu/kanade/data/source/EHentaiPagingSource.kt @@ -3,68 +3,43 @@ package eu.kanade.data.source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.util.lang.awaitSingle -import exh.metadata.metadata.EHentaiSearchMetadata +import exh.metadata.metadata.base.RaisedSearchMetadata abstract class EHentaiPagingSource(override val source: EHentai) : SourcePagingSource(source) { - private var lastMangaLink: String? = null + override fun getPageLoadResult( + params: LoadParams, + mangasPage: MangasPage, + ): LoadResult.Page> { + mangasPage as MetadataMangasPage + val metadata = mangasPage.mangasMetadata - abstract suspend fun fetchNextPage(currentPage: Int): MangasPage - - override suspend fun requestNextPage(currentPage: Int): MangasPage { - val lastMangaLink = lastMangaLink - - val gid = if (lastMangaLink != null && source.exh) { - EHentaiSearchMetadata.galleryId(lastMangaLink).toInt() - } else { - null - } - - val mangasPage = fetchNextPage(gid ?: currentPage) - - mangasPage.mangas.lastOrNull()?.let { - this.lastMangaLink = it.url - } - return if (lastMangaLink != null) { - val index = mangasPage.mangas.indexOfFirst { it.url == lastMangaLink } - if (index != -1) { - val lastIndex = mangasPage.mangas.size - val startIndex = (index + 1).coerceAtMost(mangasPage.mangas.lastIndex) - if (mangasPage is MetadataMangasPage) { - mangasPage.copy( - mangas = mangasPage.mangas.subList(startIndex, lastIndex), - mangasMetadata = mangasPage.mangasMetadata.subList(startIndex, lastIndex), - ) - } else { - mangasPage.copy( - mangas = mangasPage.mangas.subList(startIndex, lastIndex), - ) - } - } else { - mangasPage - } - } else { - mangasPage - } + return LoadResult.Page( + data = mangasPage.mangas + .mapIndexed { index, sManga -> sManga to metadata.getOrNull(index) }, + prevKey = null, + nextKey = mangasPage.nextKey, + ) } } class EHentaiSearchPagingSource(source: EHentai, val query: String, val filters: FilterList) : EHentaiPagingSource(source) { - override suspend fun fetchNextPage(currentPage: Int): MangasPage { + override suspend fun requestNextPage(currentPage: Int): MangasPage { return source.fetchSearchManga(currentPage, query, filters).awaitSingle() } } class EHentaiPopularPagingSource(source: EHentai) : EHentaiPagingSource(source) { - override suspend fun fetchNextPage(currentPage: Int): MangasPage { + override suspend fun requestNextPage(currentPage: Int): MangasPage { return source.fetchPopularManga(currentPage).awaitSingle() } } class EHentaiLatestPagingSource(source: EHentai) : EHentaiPagingSource(source) { - override suspend fun fetchNextPage(currentPage: Int): MangasPage { + override suspend fun requestNextPage(currentPage: Int): MangasPage { return source.fetchLatestUpdates(currentPage).awaitSingle() } } diff --git a/app/src/main/java/eu/kanade/data/source/SourcePagingSource.kt b/app/src/main/java/eu/kanade/data/source/SourcePagingSource.kt index 8b1275fb9..c83f8168c 100644 --- a/app/src/main/java/eu/kanade/data/source/SourcePagingSource.kt +++ b/app/src/main/java/eu/kanade/data/source/SourcePagingSource.kt @@ -29,6 +29,16 @@ abstract class SourcePagingSource( } catch (e: Exception) { return LoadResult.Error(e) } + + // SY --> + return getPageLoadResult(params, mangasPage) + // SY <-- + } + + // SY --> + open fun getPageLoadResult(params: LoadParams, mangasPage: MangasPage): LoadResult.Page */ Pair/*SY <-- */> { + val page = params.key ?: 1 + // SY --> val metadata = if (mangasPage is MetadataMangasPage) { mangasPage.mangasMetadata @@ -46,6 +56,7 @@ abstract class SourcePagingSource( nextKey = if (mangasPage.hasNextPage) page + 1 else null, ) } + // SY <-- override fun getRefreshKey(state: PagingState */ Pair/*SY <-- */>): Long? { return state.anchorPosition?.let { anchorPosition -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt index fb53041fb..8cc3633cc 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt @@ -191,14 +191,7 @@ class EHentai( tags += parsedTags if (infoElements != null) { - genre = getGenre( - infoElements.getOrNull(1), - genreString = infoElements.getOrNull(1) - ?.text() - ?.nullIfBlank() - ?.lowercase() - ?.replace(" ", ""), - ) + genre = getGenre(infoElements.getOrNull(1)) datePosted = getDateTag(infoElements.getOrNull(2)) @@ -208,13 +201,7 @@ class EHentai( length = getPageCount(infoElements.getOrNull(5)) } else { - val parsedGenre = body.selectFirst(".gl1c div") - genre = getGenre( - genreString = parsedGenre?.text() - ?.nullIfBlank() - ?.lowercase() - ?.replace(" ", ""), - ) + genre = getGenre(body.selectFirst(".gl1c div")) val info = body.selectFirst(".gl2c")!! val extraInfo = body.selectFirst(".gl4c")!! @@ -239,34 +226,49 @@ class EHentai( } }, ) + }.ifEmpty { + selectFirst(".searchwarn")?.let { throw Exception(it.text()) } + emptyList() } val parsedLocation = doc.location().toHttpUrlOrNull() + val isReversed = parsedLocation != null && parsedLocation.queryParameterNames.contains(REVERSE_PARAM) // Add to page if required - val hasNextPage = if (parsedLocation == null || - !parsedLocation.queryParameterNames.contains(REVERSE_PARAM) - ) { - select("a[onclick=return false]").last() - ?.let { - it.text() == ">" - } - ?: select(".searchnav >div > a") - .any { it.attr("href").contains("next") } + val hasNextPage = if (isReversed) { + select(".searchnav >div > a") + .any { "prev" in it.attr("href") } } else { - parsedLocation.queryParameter(REVERSE_PARAM)!!.toBoolean() + select(".searchnav >div > a") + .any { "next" in it.attr("href") } } - parsedMangas to hasNextPage + val nextPage = if (parsedLocation?.pathSegments?.contains("toplist.php") == true) { + ((parsedLocation!!.queryParameter("p")?.toLong() ?: 0) + 2).takeIf { it <= 200 } + } else if (hasNextPage) { + parsedMangas.let { if (isReversed) it.first() else it.last() } + .manga + .url + .let { EHentaiSearchMetadata.galleryId(it).toLong() } + } else { + null + } + + parsedMangas.let { if (isReversed) it.reversed() else it } to nextPage } - private fun getGenre(element: Element? = null, genreString: String? = null): String? { + private fun getGenre(element: Element?): String? { return element?.attr("onclick") ?.nullIfBlank() ?.substringAfterLast('/') ?.removeSuffix("'") ?.trim() ?.substringAfterLast('/') - ?.removeSuffix("'") ?: genreString + ?.removeSuffix("'") + ?: element?.text() + ?.nullIfBlank() + ?.lowercase() + ?.replace(" ", "") + ?.trim() } private fun getDateTag(element: Element?): Long? { @@ -315,8 +317,13 @@ class EHentai( /** * Parse a list of galleries */ - private fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let { mangaFromSource -> - MetadataMangasPage(mangaFromSource.first.map { it.manga }, mangaFromSource.second, mangaFromSource.first.map { it.metadata }) + private fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let { (parsedManga, nextPage) -> + MetadataMangasPage( + parsedManga.map { it.manga }, + nextPage != null, + parsedManga.map { it.metadata }, + nextPage, + ) } override suspend fun getChapterList(manga: SManga): List = getChapterList(manga) {} @@ -447,7 +454,7 @@ class EHentai( return client.newCall(chapterPageRequest(np)).asObservableSuccess() } private fun chapterPageRequest(np: String): Request { - return exGet(np, null, headers) + return exGet(url = np, additionalHeaders = headers) } private fun nextPageUrl(element: Element): String? = element.select("a[onclick=return false]").last()?.let { @@ -477,16 +484,14 @@ class EHentai( // Support direct URL importing override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = urlImportFetchSearchManga(context, query) { - searchMangaRequestObservable(page, query, filters).flatMap { - client.newCall(it).asObservableSuccess() - }.map { response -> - searchMangaParse(response) - }.checkValid() + super.fetchSearchManga(page, query, filters).checkValid() } - private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable { + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val uri = baseUrl.toUri().buildUpon() val toplist = ToplistOption.values()[filters.firstNotNullOfOrNull { (it as? ToplistOptions)?.state } ?: 0] + val isReverseFilterEnabled = filters.any { it is ReverseFilter && it.state } + val jumpSeekValue = filters.firstNotNullOfOrNull { (it as? JumpSeekFilter)?.state?.nullIfBlank() } if (toplist == ToplistOption.NONE) { uri.appendQueryParameter("f_apply", "Apply+Filter") @@ -494,6 +499,20 @@ class EHentai( filters.forEach { if (it is UriFilter) it.addToUri(uri) } + // Reverse search results on filter + if (isReverseFilterEnabled) { + uri.appendQueryParameter(REVERSE_PARAM, "on") + } + if (jumpSeekValue != null && page == 1) { + if ( + MATCH_SEEK_REGEX.matches(jumpSeekValue) || + (MATCH_YEAR_REGEX.matches(jumpSeekValue) && jumpSeekValue.toIntOrNull()?.let { it in 2007..2099 } == true) + ) { + uri.appendQueryParameter("seek", jumpSeekValue) + } else if (MATCH_JUMP_REGEX.matches(jumpSeekValue)) { + uri.appendQueryParameter("jump", jumpSeekValue) + } + } } else { uri.appendPath("toplist.php") uri.appendQueryParameter("tl", toplist.index.toString()) @@ -506,52 +525,31 @@ class EHentai( null } - val request = exGet(uri.toString(), regularPage) - - // Reverse search results on filter - if (toplist == ToplistOption.NONE && filters.any { it is ReverseFilter && it.state }) { - return client.newCall(request) - .asObservableSuccess() - .map { - val doc = it.asJsoup() - - val elements = doc.select(".ptt > tbody > tr > td") - - val totalElement = elements[elements.size - 2] - - val thisPage = totalElement.text().toInt() - (page - 1) - - uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString()) - - exGet(uri.toString(), thisPage) - } - } else { - return Observable.just(request) - } + return exGet( + url = uri.toString(), + next = if (!isReverseFilterEnabled) regularPage else null, + prev = if (isReverseFilterEnabled) regularPage else null, + ) } - override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() - override fun latestUpdatesRequest(page: Int) = exGet(baseUrl, page) override fun popularMangaParse(response: Response) = genericMangaParse(response) override fun searchMangaParse(response: Response) = genericMangaParse(response) override fun latestUpdatesParse(response: Response) = genericMangaParse(response) - private fun exGet(url: String, next: Int? = null, additionalHeaders: Headers? = null, cacheControl: CacheControl? = null): Request { + private fun exGet( + url: String, + next: Int? = null, + prev: Int? = null, + additionalHeaders: Headers? = null, + cacheControl: CacheControl? = null, + ): Request { return GET( - if (next != null) { - if (exh) { - if (next > 1) { - addParam(url, "next", next.toString()) - } else { - url - } - } else { - addParam(url, "page", (next - 1).toString()) - } - } else { - url + when { + next != null && next > 1 -> addParam(url, "next", next.toString()) + prev != null && prev > 0 -> addParam(url, "prev", prev.toString()) + else -> url }, if (additionalHeaders != null) { val headers = headers.newBuilder() @@ -815,7 +813,7 @@ class EHentai( // Next page page = parsed.first.lastOrNull()?.manga?.url?.let { EHentaiSearchMetadata.galleryId(it) }?.toInt() ?: 0 - } while (parsed.second) + } while (parsed.second != null) return Pair(result.toList(), favNames.orEmpty()) } @@ -886,8 +884,6 @@ class EHentai( // Filters override fun getFilterList(): FilterList { - val excludePrefix = "-" - return FilterList( *if (exh) { emptyArray() @@ -898,19 +894,12 @@ class EHentai( Filter.Separator(), ) }, - AutoCompleteTags( - EHTags.getNamespaces().map { "$it:" } + EHTags.getAllTags(), - EHTags.getNamespaces().map { "$it:" }, - excludePrefix, - ), - if (preferences.exhWatchedListDefaultState().get()) { - Watched(isEnabled = true) - } else { - Watched(isEnabled = false) - }, + AutoCompleteTags(), + Watched(isEnabled = preferences.exhWatchedListDefaultState().get()), GenreGroup(), AdvancedGroup(), ReverseFilter(), + JumpSeekFilter(), ) } @@ -992,9 +981,16 @@ class EHentai( filter.state.trimAll().dropBlank().mapNotNull { tag -> val split = tag.split(":").filterNot { it.isBlank() } if (split.size > 1) { - val namespace = split[0].removePrefix("-") + val namespace = split[0].removePrefix("-").removePrefix("~") val exclude = split[0].startsWith("-") - AdvSearchEntry(namespace to split[1], exclude) + val or = split[0].startsWith("~") + + AdvSearchEntry(namespace to split[1], exclude, or) + } else if (split.size == 1) { + val item = split.first() + val exclude = item.startsWith("-") + val or = item.startsWith("~") + AdvSearchEntry(null to item, exclude, or) } else { null } @@ -1003,10 +999,12 @@ class EHentai( advSearch.forEach { entry -> if (entry.exclude) stringBuilder.append("-") + if (entry.or) stringBuilder.append("~") + val namespace = entry.search.first?.let { "$it:" }.orEmpty() if (entry.search.second.contains(" ")) { - stringBuilder.append(("""${entry.search.first}:"${entry.search.second}$"""")) + stringBuilder.append(("""$namespace"${entry.search.second}$"""")) } else { - stringBuilder.append("${entry.search.first}:${entry.search.second}$") + stringBuilder.append("$namespace${entry.search.second}$") } stringBuilder.append(" ") } @@ -1014,15 +1012,15 @@ class EHentai( return stringBuilder.toString().trim().also { xLogD(it) } } - data class AdvSearchEntry(val search: Pair, val exclude: Boolean) + data class AdvSearchEntry(val search: Pair, val exclude: Boolean, val or: Boolean) - class AutoCompleteTags(tags: List, skipAutoFillTags: List, excludePrefix: String) : + class AutoCompleteTags : Filter.AutoComplete( name = "Tags", hint = "Search tags here (limit of 8)", - values = tags, - skipAutoFillTags = skipAutoFillTags, - excludePrefix = excludePrefix, + values = EHTags.getNamespaces().map { "$it:" } + EHTags.getAllTags(), + skipAutoFillTags = EHTags.getNamespaces().map { "$it:" }, + validPrefixes = listOf("-", "~"), state = emptyList(), ) @@ -1052,22 +1050,21 @@ class EHentai( class AdvancedGroup : UriGroup>( "Advanced Options", listOf( - AdvancedOption("Search Gallery Name", "f_sname", true), - AdvancedOption("Search Gallery Tags", "f_stags", true), - AdvancedOption("Search Gallery Description", "f_sdesc"), - AdvancedOption("Search Torrent Filenames", "f_storr"), - AdvancedOption("Only Show Galleries With Torrents", "f_sto"), - AdvancedOption("Search Low-Power Tags", "f_sdt1"), - AdvancedOption("Search Downvoted Tags", "f_sdt2"), - AdvancedOption("Show Expunged Galleries", "f_sh"), + AdvancedOption("Browse Expunged Galleries", "f_sh"), + AdvancedOption("Require Gallery Torrent", "f_sto"), RatingOption(), MinPagesOption(), MaxPagesOption(), + AdvancedOption("Disable custom Language filters", "f_sfl"), + AdvancedOption("Disable custom Uploader filters", "f_sfu"), + AdvancedOption("Disable custom Tag filters", "f_sft"), ), ) class ReverseFilter : Filter.CheckBox("Reverse search results") + class JumpSeekFilter : Filter.Text("Jump/Seek") + override val name = if (exh) { "ExHentai" } else { @@ -1292,6 +1289,10 @@ class EHentai( private const val BLANK_THUMB = "blank.gif" private const val BLANK_PREVIEW_THUMB = "https://$THUMB_DOMAIN/g/$BLANK_THUMB" + private val MATCH_YEAR_REGEX = "^\\d{4}\$".toRegex() + private val MATCH_SEEK_REGEX = "^\\d{2,4}-\\d{1,2}".toRegex() + private val MATCH_JUMP_REGEX = "^\\d+(\$|d\$|w\$|m\$|y\$|-\$)".toRegex() + private const val EH_API_BASE = "https://api.e-hentai.org/api.php" private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!! diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/AutoComplete.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/AutoComplete.kt index 443d62d40..cc127e58f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/AutoComplete.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/AutoComplete.kt @@ -38,15 +38,16 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem< holder.itemView.context, android.R.layout.simple_dropdown_item_1line, filter.values, - filter.excludePrefix, + filter.validPrefixes, ), ) holder.autoComplete.threshold = 3 // select from auto complete holder.autoComplete.setOnItemClickListener { adapterView, _, chipPosition, _ -> - val name = adapterView.getItemAtPosition(chipPosition) as String - if (name !in if (filter.excludePrefix != null && name.startsWith(filter.excludePrefix!!)) filter.skipAutoFillTags.map { filter.excludePrefix + it } else filter.skipAutoFillTags) { + var name = (adapterView.getItemAtPosition(chipPosition) as String).trim() + filter.validPrefixes.find { name.startsWith(it) }?.let { name = name.removePrefix(it).trim() } + if (name !in filter.skipAutoFillTags) { holder.autoComplete.text = null addTag(name, holder) } @@ -54,12 +55,14 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem< // done keyboard button is pressed holder.autoComplete.setOnEditorActionListener { textView, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE && textView.text.toString() !in if (filter.excludePrefix != null && textView.text.toString().startsWith(filter.excludePrefix!!)) filter.skipAutoFillTags.map { filter.excludePrefix + it } else filter.skipAutoFillTags) { + if (actionId != EditorInfo.IME_ACTION_DONE) return@setOnEditorActionListener false + var name = textView.text.toString().trim() + filter.validPrefixes.find { name.startsWith(it) }?.let { name = name.removePrefix(it).trim() } + if (name !in filter.skipAutoFillTags) { textView.text = null - addTag(textView.text.toString(), holder) - return@setOnEditorActionListener true + addTag(name, holder) } - false + true } // space or comma is detected @@ -69,7 +72,7 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem< } if (it.last() == ',') { - val name = it.substring(0, it.length - 1) + val name = it.toString().dropLast(1).trim() addTag(name, holder) holder.autoComplete.text = null diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/AutoCompleteAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/AutoCompleteAdapter.kt index fb4eccf69..82499e078 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/AutoCompleteAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/AutoCompleteAdapter.kt @@ -5,11 +5,11 @@ import android.widget.ArrayAdapter import android.widget.Filter import android.widget.Filterable -class AutoCompleteAdapter(context: Context, resource: Int, var objects: List, val excludePrefix: String?) : +class AutoCompleteAdapter(context: Context, resource: Int, var objects: List, val validPrefixes: List) : ArrayAdapter(context, resource, objects), Filterable { - private var mOriginalValues: List? = objects + private val mOriginalValues: List = objects private var mFilter: ListFilter? = null override fun getCount(): Int { @@ -28,21 +28,27 @@ class AutoCompleteAdapter(context: Context, resource: Int, var objects: List(val name: String, var state: T) { val hint: String, val values: List, val skipAutoFillTags: List = emptyList(), - val excludePrefix: String? = null, + val validPrefixes: List = emptyList(), state: List, ) : Filter>(name, state) // SY <-- diff --git a/source-api/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt b/source-api/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt index b57ae6093..03db954b9 100755 --- a/source-api/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt +++ b/source-api/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt @@ -25,8 +25,54 @@ open /* SY <-- */ class MangasPage(open val mangas: List, open val hasNe fun copy(mangas: List = this.mangas, hasNextPage: Boolean = this.hasNextPage): MangasPage { return MangasPage(mangas, hasNextPage) } + + override fun toString(): String { + return "MangasPage(mangas=$mangas, hasNextPage=$hasNextPage)" + } } // SY --> -data class MetadataMangasPage(override val mangas: List, override val hasNextPage: Boolean, val mangasMetadata: List) : MangasPage(mangas, hasNextPage) +class MetadataMangasPage( + override val mangas: List, + override val hasNextPage: Boolean, + val mangasMetadata: List, + val nextKey: Long? = null +) : MangasPage(mangas, hasNextPage) { + fun copy( + mangas: List = this.mangas, + hasNextPage: Boolean = this.hasNextPage, + mangasMetadata: List = this.mangasMetadata, + nextKey: Long? = this.nextKey + ): MangasPage { + return MetadataMangasPage(mangas, hasNextPage, mangasMetadata, nextKey) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as MetadataMangasPage + + if (mangas != other.mangas) return false + if (hasNextPage != other.hasNextPage) return false + if (mangasMetadata != other.mangasMetadata) return false + if (nextKey != other.nextKey) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + mangas.hashCode() + result = 31 * result + hasNextPage.hashCode() + result = 31 * result + mangasMetadata.hashCode() + result = 31 * result + nextKey.hashCode() + return result + } + + override fun toString(): String { + return "MetadataMangasPage(mangas=$mangas, hasNextPage=$hasNextPage, mangasMetadata=$mangasMetadata, nextKey=$nextKey)" + } +} // SY <--