From 6abded47de12383eafa99c5e240ac1dbf265fb16 Mon Sep 17 00:00:00 2001 From: "Cuong M. Tran" Date: Sun, 12 May 2024 11:36:48 +0700 Subject: [PATCH] Update theme GalleryAdults (#2911) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Organizing code * remove unnecessary files * improve base class - allow override search’s uri - add shared method to get description & page count - base class’s request for gallery’s all pages now support all sources without needs to override (almost) - extract method to parse JSON * Avoid request for more pages when no needed * auto add more tags to filter while viewing manga; add spit-tag * filter for getting Random manga * Always add page=1 to uri so it will exclude some non-latest mangas from homepage, happened with some sources. * reorganize code * Allow source which doesn't need shortTitle to hide it. * Extract default advanced search's Uri change base class's galleryUri value * Fix getInfoPages * Fix missing category filter * open for override * bump base class version --- lib-multisrc/galleryadults/build.gradle.kts | 2 +- .../multisrc/galleryadults/GalleryAdults.kt | 371 +++++++++++------- .../galleryadults/GalleryAdultsFilters.kt | 6 +- .../galleryadults/GalleryAdultsUtils.kt | 4 +- .../extension/all/asmhentai/AsmHentai.kt | 85 ++-- .../extension/all/hentaifox/HentaiFox.kt | 46 ++- src/all/imhentai/AndroidManifest.xml | 23 -- .../extension/all/imhentai/IMHentai.kt | 97 +---- .../all/imhentai/IMHentaiUrlActivity.kt | 38 -- 9 files changed, 318 insertions(+), 354 deletions(-) delete mode 100644 src/all/imhentai/AndroidManifest.xml delete mode 100644 src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentaiUrlActivity.kt diff --git a/lib-multisrc/galleryadults/build.gradle.kts b/lib-multisrc/galleryadults/build.gradle.kts index 7dd8f08b9..dc076cc37 100644 --- a/lib-multisrc/galleryadults/build.gradle.kts +++ b/lib-multisrc/galleryadults/build.gradle.kts @@ -2,4 +2,4 @@ plugins { id("lib-multisrc") } -baseVersionCode = 0 +baseVersionCode = 1 diff --git a/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdults.kt b/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdults.kt index 891961ea5..aac2d82d2 100644 --- a/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdults.kt +++ b/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdults.kt @@ -55,14 +55,12 @@ abstract class GalleryAdults( .add("X-Requested-With", "XMLHttpRequest") .build() + /* Preferences */ protected val preferences: SharedPreferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } - protected val SharedPreferences.shortTitle - get() = getBoolean(PREF_SHORT_TITLE, false) - - private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""") + protected open val useShortTitlePreference = true override fun setupPreferenceScreen(screen: PreferenceScreen) { SwitchPreferenceCompat(screen.context).apply { @@ -71,18 +69,12 @@ abstract class GalleryAdults( summaryOff = "Showing Long Titles" summaryOn = "Showing short Titles" setDefaultValue(false) + setVisible(useShortTitlePreference) }.also(screen::addPreference) } - protected open fun Element.mangaTitle(selector: String = ".caption"): String? = - mangaFullTitle(selector).let { - if (preferences.shortTitle) it?.shortenTitle() else it - } - - protected fun Element.mangaFullTitle(selector: String) = - selectFirst(selector)?.text() - - private fun String.shortenTitle() = this.replace(shortenTitleRegex, "").trim() + protected val SharedPreferences.shortTitle + get() = getBoolean(PREF_SHORT_TITLE, false) /* List detail */ protected class SMangaDto( @@ -92,6 +84,18 @@ abstract class GalleryAdults( val lang: String, ) + protected open fun Element.mangaTitle(selector: String = ".caption"): String? = + mangaFullTitle(selector).let { + if (preferences.shortTitle) it?.shortenTitle() else it + } + + protected open fun Element.mangaFullTitle(selector: String) = + selectFirst(selector)?.text() + + protected open fun String.shortenTitle() = this.replace(shortenTitleRegex, "").trim() + + protected open val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""") + protected open fun Element.mangaUrl() = selectFirst(".inner_thumb a")?.attr("abs:href") @@ -107,7 +111,7 @@ abstract class GalleryAdults( if (!url.endsWith('/') && !url.contains('?')) { addPathSegment("") // trailing slash (/) } - if (page > 1) addQueryParameter("page", page.toString()) + addQueryParameter("page", page.toString()) return this } @@ -150,7 +154,14 @@ abstract class GalleryAdults( /* Search */ override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + val randomEntryFilter = filters.filterIsInstance().firstOrNull() + return when { + randomEntryFilter?.state == true -> { + client.newCall(randomEntryRequest()) + .asObservableSuccess() + .map { response -> randomEntryParse(response) } + } query.startsWith(PREFIX_ID_SEARCH) -> { val id = query.removePrefix(PREFIX_ID_SEARCH) client.newCall(searchMangaByIdRequest(id)) @@ -170,7 +181,29 @@ abstract class GalleryAdults( } } - protected open val idPrefixUri = "g" + protected open fun randomEntryRequest(): Request = GET("$baseUrl/random/", headers) + + protected open fun randomEntryParse(response: Response): MangasPage { + val document = response.asJsoup() + + val url = response.request.url.toString() + val id = url.removeSuffix("/").substringAfterLast('/') + return MangasPage( + listOf( + SManga.create().apply { + title = document.mangaTitle("h1")!! + setUrlWithoutDomain("$baseUrl/$idPrefixUri/$id/") + thumbnail_url = document.getCover() + }, + ), + false, + ) + } + + /** + * Manga URL: $baseUrl/$idPrefixUri// + */ + protected open val idPrefixUri = "gallery" protected open fun searchMangaByIdRequest(id: String): Request { val url = baseUrl.toHttpUrl().newBuilder().apply { @@ -189,7 +222,7 @@ abstract class GalleryAdults( protected open val useIntermediateSearch: Boolean = false protected open val supportAdvancedSearch: Boolean = false protected open val supportSpeechless: Boolean = false - private val useBasicSearch: Boolean + protected open val useBasicSearch: Boolean get() = !useIntermediateSearch override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { @@ -225,52 +258,7 @@ abstract class GalleryAdults( } } - /** - * Browsing user's personal favorites saved on site. This requires login in view WebView. - */ - protected open fun favoriteFilterSearchRequest(page: Int, query: String, filters: FilterList): Request { - val url = "$baseUrl/$favoritePath".toHttpUrl().newBuilder() - return POST( - url.build().toString(), - xhrHeaders, - FormBody.Builder() - .add("page", page.toString()) - .build(), - ) - } - - /** - * Browsing speechless titles. Some sites exclude speechless titles from normal search and - * allow browsing separately. - */ - protected open fun speechlessFilterSearchRequest(page: Int, query: String, filters: FilterList): Request { - // Basic search - val sortOrderFilter = filters.filterIsInstance().firstOrNull() - - val url = baseUrl.toHttpUrl().newBuilder().apply { - addPathSegment("language") - addPathSegment(LANGUAGE_SPEECHLESS) - if (sortOrderFilter?.state == 0) addPathSegment("popular") - addPageUri(page) - } - return GET(url.build(), headers) - } - - protected open fun tagBrowsingSearchRequest(page: Int, query: String, filters: FilterList): Request { - // Basic search - val sortOrderFilter = filters.filterIsInstance().firstOrNull() - val genresFilter = filters.filterIsInstance().firstOrNull() - val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList() - - // Browsing single tag's catalog - val url = baseUrl.toHttpUrl().newBuilder().apply { - addPathSegment("tag") - addPathSegment(selectedGenres.single().uri) - if (sortOrderFilter?.state == 0) addPathSegment("popular") - addPageUri(page) - } - return GET(url.build(), headers) - } + protected open val basicSearchKey = "q" /** * Basic Search: support query string with multiple-genres filter by adding genres to query string. @@ -283,14 +271,15 @@ abstract class GalleryAdults( val url = baseUrl.toHttpUrl().newBuilder().apply { addPathSegments("search/") - addEncodedQueryParameter("q", buildQueryString(selectedGenres.map { it.name }, query)) - // Search results sorting is not supported by AsmHentai + addEncodedQueryParameter(basicSearchKey, buildQueryString(selectedGenres.map { it.name }, query)) if (sortOrderFilter?.state == 0) addQueryParameter("sort", "popular") addPageUri(page) } return GET(url.build(), headers) } + protected open val intermediateSearchKey = "key" + /** * This supports filter query search with languages, categories (manga, doujinshi...) * with additional sort orders. @@ -318,12 +307,15 @@ abstract class GalleryAdults( toBinary(mangaLang == pair.first || mangaLang == LANGUAGE_MULTI), ) } - addEncodedQueryParameter("key", buildQueryString(selectedGenres.map { it.name }, query)) + addEncodedQueryParameter(intermediateSearchKey, buildQueryString(selectedGenres.map { it.name }, query)) addPageUri(page) } return GET(url.build()) } + protected open val advancedSearchKey = "key" + protected open val advancedSearchUri = "advsearch" + /** * Advanced Search normally won't support search for string but allow include/exclude specific * tags/artists/groups/parodies/characters @@ -339,7 +331,7 @@ abstract class GalleryAdults( // Advanced search val advancedSearchFilters = filters.filterIsInstance() - val url = "$baseUrl/advsearch".toHttpUrl().newBuilder().apply { + val url = "$baseUrl/$advancedSearchUri".toHttpUrl().newBuilder().apply { getSortOrderURIs().forEachIndexed { index, pair -> addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index)) } @@ -384,7 +376,7 @@ abstract class GalleryAdults( ) } } - addEncodedQueryParameter("key", keys.joinToString("+")) + addEncodedQueryParameter(advancedSearchKey, keys.joinToString("+")) addPageUri(page) } return GET(url.build()) @@ -405,7 +397,54 @@ abstract class GalleryAdults( } } - protected open val favoritePath = "includes/user_favs.php" + protected open fun tagBrowsingSearchRequest(page: Int, query: String, filters: FilterList): Request { + // Basic search + val sortOrderFilter = filters.filterIsInstance().firstOrNull() + val genresFilter = filters.filterIsInstance().firstOrNull() + val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList() + + // Browsing single tag's catalog + val url = baseUrl.toHttpUrl().newBuilder().apply { + addPathSegment("tag") + addPathSegment(selectedGenres.single().uri) + if (sortOrderFilter?.state == 0) addPathSegment("popular") + addPageUri(page) + } + return GET(url.build(), headers) + } + + /** + * Browsing speechless titles. Some sites exclude speechless titles from normal search and + * allow browsing separately. + */ + protected open fun speechlessFilterSearchRequest(page: Int, query: String, filters: FilterList): Request { + // Basic search + val sortOrderFilter = filters.filterIsInstance().firstOrNull() + + val url = baseUrl.toHttpUrl().newBuilder().apply { + addPathSegment("language") + addPathSegment(LANGUAGE_SPEECHLESS) + if (sortOrderFilter?.state == 0) addPathSegment("popular") + addPageUri(page) + } + return GET(url.build(), headers) + } + + /** + * Browsing user's personal favorites saved on site. This requires login in view WebView. + */ + protected open fun favoriteFilterSearchRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/$favoritePath".toHttpUrl().newBuilder() + return POST( + url.build().toString(), + xhrHeaders, + FormBody.Builder() + .add("page", page.toString()) + .build(), + ) + } + + protected open val favoritePath = "user/fav_pags.php" protected open fun loginRequired(document: Document, url: String): Boolean { return ( @@ -453,34 +492,7 @@ abstract class GalleryAdults( override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() /* Details */ - protected open fun Element.getCover() = - selectFirst(".cover img")?.imgAttr() - - protected open fun Element.getInfo(tag: String): String { - return select("ul.${tag.lowercase()} a") - .joinToString { it.ownText() } - } - - protected open fun Element.getDescription(): String = ( - listOf("Parodies", "Characters", "Languages", "Categories") - .mapNotNull { tag -> - getInfo(tag) - .let { if (it.isNotBlank()) "$tag: $it" else null } - } + - listOfNotNull( - selectFirst(".pages:contains(Pages:)")?.ownText(), - ) - ) - .joinToString("\n\n") - protected open val mangaDetailInfoSelector = ".gallery_top" - protected open val timeSelector = "time[datetime]" - - protected open fun Element.getTime(): Long { - return selectFirst(timeSelector) - ?.attr("datetime") - .toDate(simpleDateFormat) - } override fun mangaDetailsParse(document: Document): SManga { return document.selectFirst(mangaDetailInfoSelector)!!.run { @@ -491,11 +503,54 @@ abstract class GalleryAdults( thumbnail_url = getCover() genre = getInfo("Tags") author = getInfo("Artists") - description = getDescription() + description = getDescription(document) } } } + protected open fun Element.getCover() = + selectFirst(".cover img")?.imgAttr() + + protected val regexTag = Regex("Tags?") + + /** + * Parsing document to extract info related to [tag]. + */ + protected abstract fun Element.getInfo(tag: String): String + + protected open fun Element.getDescription(document: Document? = null): String = ( + listOf("Parodies", "Characters", "Languages", "Categories", "Category") + .mapNotNull { tag -> + getInfo(tag) + .takeIf { it.isNotBlank() } + ?.let { "$tag: $it" } + } + + listOfNotNull( + getInfoPages(document), + getInfoAlternativeTitle(), + getInfoFullTitle(), + ) + ) + .joinToString("\n\n") + + protected open fun Element.getInfoPages(document: Document? = null): String? = + document?.inputIdValueOf(totalPagesSelector) + ?.takeIf { it.isNotBlank() } + ?.let { "Pages: $it" } + + protected open fun Element.getInfoAlternativeTitle(): String? = + selectFirst("h1 + h2, .subtitle")?.ownText() + .takeIf { !it.isNullOrBlank() } + ?.let { "Alternative title: $it" } + + protected open fun Element.getInfoFullTitle(): String? = + if (preferences.shortTitle) "Full title: ${mangaFullTitle("h1")}" else null + + protected open fun Element.getTime(): Long = + selectFirst(".uploaded") + ?.ownText() + .toDate(simpleDateFormat) + /* Chapters */ override fun chapterListParse(response: Response): List { val document = response.asJsoup() @@ -515,39 +570,71 @@ abstract class GalleryAdults( override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException() /* Pages */ - protected open fun Document.inputIdValueOf(string: String): String { + protected open fun Element.inputIdValueOf(string: String): String { return select("input[id=$string]").attr("value") } + protected open val pagesRequest = "inc/thumbs_loader.php" protected open val galleryIdSelector = "gallery_id" protected open val loadIdSelector = "load_id" protected open val loadDirSelector = "load_dir" protected open val totalPagesSelector = "load_pages" - protected open val pageUri = "g" - protected open val pageSelector = ".gallery_thumb" + protected open val serverSelector = "load_server" - protected open val pagesRequest = "inc/thumbs_loader.php" + protected open fun pageRequestForm(document: Document, totalPages: String, loadedPages: Int): FormBody { + val token = document.select("[name=csrf-token]").attr("content") + val serverNumber = document.serverNumber() + + return FormBody.Builder() + .add("u_id", document.inputIdValueOf(galleryIdSelector)) + .add("g_id", document.inputIdValueOf(loadIdSelector)) + .add("img_dir", document.inputIdValueOf(loadDirSelector)) + .add("visible_pages", loadedPages.toString()) + .add("total_pages", totalPages) + .add("type", "2") // 1 would be "more", 2 is "all remaining" + .apply { + if (token.isNotBlank()) add("_token", token) + if (serverNumber != null) add("server", serverNumber) + } + .build() + } + + protected open val thumbnailSelector = ".gallery_thumb" private val jsonFormat: Json by injectLazy() - protected open fun getServer(document: Document, galleryId: String): String { - val cover = document.getCover() - return cover!!.toHttpUrl().host + protected open fun Element.getServer(): String { + val domain = baseUrl.toHttpUrl().host + return serverNumber() + ?.let { "m$it.$domain" } + ?: getCover()!!.toHttpUrl().host } - override fun pageListParse(document: Document): List { - val json = document.selectFirst("script:containsData(parseJSON)")?.data() + protected open fun Element.serverNumber(): String? = + inputIdValueOf(serverSelector) + .takeIf { it.isNotBlank() } + + protected open fun Element.parseJson(): String? = + selectFirst("script:containsData(parseJSON)")?.data() ?.substringAfter("$.parseJSON('") ?.substringBefore("');")?.trim() + /** + * Page URL: $baseUrl/$pageUri// + */ + protected open val pageUri = "g" + + override fun pageListParse(document: Document): List { + val json = document.parseJson() + if (json != null) { val loadDir = document.inputIdValueOf(loadDirSelector) val loadId = document.inputIdValueOf(loadIdSelector) val galleryId = document.inputIdValueOf(galleryIdSelector) val pageUrl = "$baseUrl/$pageUri/$galleryId" - val randomServer = getServer(document, galleryId) - val imagesUri = "https://$randomServer/$loadDir/$loadId" + val server = document.getServer() + val imagesUri = "https://$server/$loadDir/$loadId" try { val pages = mutableListOf() @@ -583,9 +670,11 @@ abstract class GalleryAdults( /** * Overwrite this to force extension not blindly converting thumbnails to full image - * with simply removing the trailing "t" from file name. Instead, it will open each page, + * by simply removing the trailing "t" from file name. Instead, it will open each page, * one by one, then parsing for actual image's URL. * This will be much slower but guaranteed work. + * + * This only apply if site doesn't provide 'parseJSON'. */ protected open val parsingImagePageByPage: Boolean = false @@ -596,12 +685,11 @@ abstract class GalleryAdults( * which will then request one by one to parse for page's image's URL using [imageUrlParse]. */ protected open fun pageListParseAlternative(document: Document): List { - // input only exists if pages > 10 and have to make a request to get the other thumbnails val totalPages = document.inputIdValueOf(totalPagesSelector) val galleryId = document.inputIdValueOf(galleryIdSelector) val pageUrl = "$baseUrl/$pageUri/$galleryId" - val pages = document.select("$pageSelector a") + val pages = document.select("$thumbnailSelector a") .map { if (parsingImagePageByPage) { it.absUrl("href") @@ -611,8 +699,8 @@ abstract class GalleryAdults( } .toMutableList() - if (totalPages.isNotBlank()) { - val form = pageRequestForm(document, totalPages) + if (totalPages.isNotBlank() && totalPages.toInt() > pages.size) { + val form = pageRequestForm(document, totalPages, pages.size) val morePages = client.newCall(POST("$baseUrl/$pagesRequest", xhrHeaders, form)) .execute() @@ -655,16 +743,15 @@ abstract class GalleryAdults( val galleryId = document.inputIdValueOf(galleryIdSelector) val pageUrl = "$baseUrl/$pageUri/$galleryId" - val randomServer = getServer(document, galleryId) - val imagesUri = "https://$randomServer/$loadDir/$loadId" + val server = document.getServer() + val imagesUri = "https://$server/$loadDir/$loadId" - val images = document.select("$pageSelector img") + val images = document.select("$thumbnailSelector img") val thumbUrls = images.map { it.imgAttr() }.toMutableList() - // totalPages only exists if pages > 10 and have to make a request to get the other thumbnails val totalPages = document.inputIdValueOf(totalPagesSelector) - if (totalPages.isNotBlank()) { + if (totalPages.isNotBlank() && totalPages.toInt() > thumbUrls.size) { val imagesExt = images.first()?.imgAttr()!! .substringAfterLast('.') @@ -683,16 +770,6 @@ abstract class GalleryAdults( } } - protected open fun pageRequestForm(document: Document, totalPages: String): FormBody = - FormBody.Builder() - .add("u_id", document.inputIdValueOf(galleryIdSelector)) - .add("g_id", document.inputIdValueOf(loadIdSelector)) - .add("img_dir", document.inputIdValueOf(loadDirSelector)) - .add("visible_pages", "10") - .add("total_pages", totalPages) - .add("type", "2") // 1 would be "more", 2 is "all remaining" - .build() - override fun imageUrlParse(document: Document): String { return document.selectFirst("img#gimg, img#fimg")?.imgAttr()!! } @@ -700,10 +777,15 @@ abstract class GalleryAdults( /* Filters */ private val scope = CoroutineScope(Dispatchers.IO) private fun launchIO(block: () -> Unit) = scope.launch { block() } + private var tagsFetched = false private var tagsFetchAttempt = 0 - private var genres = emptyList() - private fun tagsRequest(page: Int): Request { + /** + * List of tags in pairs + */ + protected var genres: MutableMap = mutableMapOf() + + protected open fun tagsRequest(page: Int): Request { val url = baseUrl.toHttpUrl().newBuilder().apply { addPathSegments("tags/popular") addPageUri(page) @@ -711,21 +793,24 @@ abstract class GalleryAdults( return GET(url.build(), headers) } - protected open fun tagsParser(document: Document): List> { - return document.select(".list_tags .tag_item") + /** + * Parsing [document] to return a list of tags in pairs. + */ + protected open fun tagsParser(document: Document): List { + return document.select("a.tag_btn") .mapNotNull { - Pair( - it.selectFirst("h3.list_tag")?.ownText() ?: "", - it.select("a").attr("href") + Genre( + it.select(".list_tag, .tag_name").text(), + it.attr("href") .removeSuffix("/").substringAfterLast('/'), ) } } - private fun getGenres() { - if (genres.isEmpty() && tagsFetchAttempt < 3) { + protected open fun requestTags() { + if (!tagsFetched && tagsFetchAttempt < 3) { launchIO { - val tags = mutableListOf>() + val tags = mutableListOf() runBlocking { val jobsPool = mutableListOf() // Get first 3 pages @@ -742,7 +827,11 @@ abstract class GalleryAdults( ) } jobsPool.joinAll() - genres = tags.sortedWith(compareBy { it.first }).map { Genre(it.first, it.second) } + tags.sortedWith(compareBy { it.name }) + .forEach { + genres[it.name] = it.uri + } + tagsFetched = true } tagsFetchAttempt++ @@ -751,7 +840,7 @@ abstract class GalleryAdults( } override fun getFilterList(): FilterList { - getGenres() + requestTags() val filters = emptyList>().toMutableList() if (useIntermediateSearch) { filters.add(Filter.Header("HINT: Separate search term with comma (,)")) @@ -765,7 +854,7 @@ abstract class GalleryAdults( filters.add(GenresFilter(genres)) } - if (useIntermediateSearch) { + if (useIntermediateSearch || supportAdvancedSearch) { filters.addAll( listOf( Filter.Separator(), @@ -795,6 +884,8 @@ abstract class GalleryAdults( } filters.add(FavoriteFilter()) + filters.add(RandomEntryFilter()) + return FilterList(filters) } diff --git a/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdultsFilters.kt b/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdultsFilters.kt index 23008a3e0..994414e76 100644 --- a/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdultsFilters.kt +++ b/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdultsFilters.kt @@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.multisrc.galleryadults import eu.kanade.tachiyomi.source.model.Filter class Genre(name: String, val uri: String) : Filter.CheckBox(name) -class GenresFilter(genres: List) : Filter.Group( +class GenresFilter(genres: Map) : Filter.Group( "Tags", - genres.map { Genre(it.name, it.uri) }, + genres.map { Genre(it.key, it.value) }, ) class SortOrderFilter(sortOrderURIs: List>) : @@ -13,6 +13,8 @@ class SortOrderFilter(sortOrderURIs: List>) : class FavoriteFilter : Filter.CheckBox("Show favorites only (login via WebView)", false) +class RandomEntryFilter : Filter.CheckBox("Random manga", false) + // Speechless class SpeechlessFilter : Filter.CheckBox("Show speechless items only", false) diff --git a/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdultsUtils.kt b/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdultsUtils.kt index 49c84465f..2410adf57 100644 --- a/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdultsUtils.kt +++ b/lib-multisrc/galleryadults/src/eu/kanade/tachiyomi/multisrc/galleryadults/GalleryAdultsUtils.kt @@ -23,7 +23,7 @@ fun Element.imgAttr() = when { hasAttr("data-lazy-src") -> absUrl("data-lazy-src") hasAttr("srcset") -> absUrl("srcset").substringBefore(" ") else -> absUrl("src") -}!! +} fun Element.cleanTag(): String = text().cleanTag() fun String.cleanTag(): String = replace(regexTagCountNumber, "").trim() @@ -57,7 +57,7 @@ fun String?.toDate(simpleDateFormat: SimpleDateFormat?): Long { } private fun parseDate(date: String?): Long { - date ?: return 0 + date ?: return 0L return when { // Handle 'yesterday' and 'today', using midnight diff --git a/src/all/asmhentai/src/eu/kanade/tachiyomi/extension/all/asmhentai/AsmHentai.kt b/src/all/asmhentai/src/eu/kanade/tachiyomi/extension/all/asmhentai/AsmHentai.kt index d03c4b239..eda853b73 100644 --- a/src/all/asmhentai/src/eu/kanade/tachiyomi/extension/all/asmhentai/AsmHentai.kt +++ b/src/all/asmhentai/src/eu/kanade/tachiyomi/extension/all/asmhentai/AsmHentai.kt @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.extension.all.asmhentai import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults -import eu.kanade.tachiyomi.multisrc.galleryadults.cleanTag +import eu.kanade.tachiyomi.multisrc.galleryadults.Genre import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList @@ -31,64 +31,64 @@ class AsmHentai( override fun popularMangaSelector() = ".preview_item" - override fun Element.getInfo(tag: String): String { - return select(".tags:contains($tag:) .tag") - .joinToString { it.ownText().cleanTag() } - } - - override fun Element.getDescription(): String { - return ( - listOf("Parodies", "Characters", "Languages", "Category") - .mapNotNull { tag -> - getInfo(tag) - .let { if (it.isNotBlank()) "$tag: $it" else null } - } + - listOfNotNull( - selectFirst(".book_page .pages h3")?.ownText(), - selectFirst(".book_page h1 + h2")?.ownText() - .let { altTitle -> if (!altTitle.isNullOrBlank()) "Alternate Title: $altTitle" else null }, - ) - ) - .joinToString("\n\n") - .plus( - if (preferences.shortTitle) { - "\nFull title: ${mangaFullTitle("h1")}" - } else { - "" - }, - ) - } - - /* Search */ override val favoritePath = "inc/user.php?act=favs" + override fun Element.getInfo(tag: String): String { + return select(".tags:contains($tag:) .tag_list a") + .joinToString { + val name = it.selectFirst(".tag")?.ownText() ?: "" + if (tag.contains(regexTag)) { + genres[name] = it.attr("href") + .removeSuffix("/").substringAfterLast('/') + } + listOf( + name, + it.select(".split_tag").text() + .removePrefix("| ") + .trim(), + ) + .filter { s -> s.isNotBlank() } + .joinToString() + } + } + + override fun Element.getInfoPages(document: Document?) = + selectFirst(".book_page .pages h3")?.ownText() + override val mangaDetailInfoSelector = ".book_page" - override val galleryIdSelector = "load_id" + /** + * [totalPagesSelector] only exists if pages > 10 + */ override val totalPagesSelector = "t_pages" - override val pageUri = "gallery" - override val pageSelector = ".preview_thumb" - override fun pageRequestForm(document: Document, totalPages: String): FormBody { + override val galleryIdSelector = "load_id" + override val thumbnailSelector = ".preview_thumb" + + override val idPrefixUri = "g" + override val pageUri = "gallery" + + override fun pageRequestForm(document: Document, totalPages: String, loadedPages: Int): FormBody { val token = document.select("[name=csrf-token]").attr("content") return FormBody.Builder() - .add("_token", token) .add("id", document.inputIdValueOf(loadIdSelector)) .add("dir", document.inputIdValueOf(loadDirSelector)) - .add("visible_pages", "10") + .add("visible_pages", loadedPages.toString()) .add("t_pages", totalPages) .add("type", "2") // 1 would be "more", 2 is "all remaining" + .apply { + if (token.isNotBlank()) add("_token", token) + } .build() } - /* Filters */ - override fun tagsParser(document: Document): List> { - return document.select(".tags_page ul.tags li") + override fun tagsParser(document: Document): List { + return document.select(".tags_page .tags a.tag") .mapNotNull { - Pair( - it.selectFirst("a.tag")?.ownText() ?: "", - it.select("a.tag").attr("href") + Genre( + it.ownText(), + it.attr("href") .removeSuffix("/").substringAfterLast('/'), ) } @@ -97,6 +97,7 @@ class AsmHentai( override fun getFilterList() = FilterList( listOf( Filter.Header("HINT: Separate search term with comma (,)"), + Filter.Header("String query search doesn't support Sort"), ) + super.getFilterList().list, ) } diff --git a/src/all/hentaifox/src/eu/kanade/tachiyomi/extension/all/hentaifox/HentaiFox.kt b/src/all/hentaifox/src/eu/kanade/tachiyomi/extension/all/hentaifox/HentaiFox.kt index 698aad26f..900a6b845 100644 --- a/src/all/hentaifox/src/eu/kanade/tachiyomi/extension/all/hentaifox/HentaiFox.kt +++ b/src/all/hentaifox/src/eu/kanade/tachiyomi/extension/all/hentaifox/HentaiFox.kt @@ -5,10 +5,7 @@ import eu.kanade.tachiyomi.multisrc.galleryadults.toDate import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import org.jsoup.nodes.Document import org.jsoup.nodes.Element -import kotlin.random.Random class HentaiFox( lang: String = "all", @@ -17,8 +14,6 @@ class HentaiFox( "HentaiFox", "https://hentaifox.com", lang = lang, - mangaLang = mangaLang, - simpleDateFormat = null, ) { override val supportsLatest = mangaLang.isNotBlank() @@ -42,13 +37,32 @@ class HentaiFox( } } + override val useShortTitlePreference = false override fun Element.mangaTitle(selector: String): String? = mangaFullTitle(selector) - override fun Element.getTime(): Long { - return selectFirst(".pages:contains(Posted:)")?.ownText() + override fun Element.getInfo(tag: String): String { + return select("ul.${tag.lowercase()} a") + .joinToString { + val name = it.ownText() + if (tag.contains(regexTag)) { + genres[name] = it.attr("href") + .removeSuffix("/").substringAfterLast('/') + } + listOf( + name, + it.select(".split_tag").text() + .removePrefix("| ") + .trim(), + ) + .filter { s -> s.isNotBlank() } + .joinToString() + } + } + + override fun Element.getTime(): Long = + selectFirst(".pages:contains(Posted:)")?.ownText() ?.removePrefix("Posted: ") .toDate(simpleDateFormat) - } override fun HttpUrl.Builder.addPageUri(page: Int): HttpUrl.Builder { val url = toString() @@ -57,22 +71,13 @@ class HentaiFox( addPathSegments("page/$page") url.contains('?') -> addQueryParameter("page", page.toString()) - page > 1 -> + else -> addPathSegments("pag/$page") } addPathSegment("") // trailing slash (/) return this } - /* Pages */ - override val pagesRequest = "includes/thumbs_loader.php" - - override fun getServer(document: Document, galleryId: String): String { - val domain = baseUrl.toHttpUrl().host - // Randomly choose between servers - return if (Random.nextBoolean()) "i2.$domain" else "i.$domain" - } - /** * Convert space( ) typed in search-box into plus(+) in URL. Then: * - ignore the word preceding by a special character (e.g. 'school-girl' will ignore 'girl') @@ -87,11 +92,12 @@ class HentaiFox( } } + override val favoritePath = "includes/user_favs.php" + override val pagesRequest = "includes/thumbs_loader.php" + override fun getFilterList() = FilterList( listOf( Filter.Header("HINT: Use double quote (\") for exact match"), ) + super.getFilterList().list, ) - - override val idPrefixUri = "gallery" } diff --git a/src/all/imhentai/AndroidManifest.xml b/src/all/imhentai/AndroidManifest.xml deleted file mode 100644 index a8e83cde1..000000000 --- a/src/all/imhentai/AndroidManifest.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentai.kt b/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentai.kt index 2e544da9c..80d7f7cd6 100644 --- a/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentai.kt +++ b/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentai.kt @@ -1,14 +1,10 @@ package eu.kanade.tachiyomi.extension.all.imhentai import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults -import eu.kanade.tachiyomi.multisrc.galleryadults.cleanTag import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr -import okhttp3.FormBody -import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody -import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.io.IOException @@ -61,46 +57,24 @@ class IMHentai( }, ).build() - override val favoritePath = "user/fav_pags.php" - /* Details */ override fun Element.getInfo(tag: String): String { - return select("li:has(.tags_text:contains($tag:)) .tag").map { - it?.run { + return select("li:has(.tags_text:contains($tag:)) a.tag") + .joinToString { + val name = it.ownText() + if (tag.contains(regexTag)) { + genres[name] = it.attr("href") + .removeSuffix("/").substringAfterLast('/') + } listOf( - ownText().cleanTag(), - select(".split_tag").text() + name, + it.select(".split_tag").text() .trim() - .removePrefix("| ") - .cleanTag(), + .removePrefix("| "), ) .filter { s -> s.isNotBlank() } .joinToString() } - }.joinToString() - } - - override fun Element.getDescription(): String { - return ( - listOf("Parodies", "Characters", "Languages", "Category") - .mapNotNull { tag -> - getInfo(tag) - .let { if (it.isNotBlank()) "$tag: $it" else null } - } + - listOfNotNull( - selectFirst(".pages")?.ownText(), - selectFirst(".subtitle")?.ownText() - .let { altTitle -> if (!altTitle.isNullOrBlank()) "Alternate Title: $altTitle" else null }, - ) - ) - .joinToString("\n\n") - .plus( - if (preferences.shortTitle) { - "\nFull title: ${mangaFullTitle("h1")}" - } else { - "" - }, - ) } override fun Element.getCover() = @@ -109,55 +83,6 @@ class IMHentai( override val mangaDetailInfoSelector = ".gallery_first" /* Pages */ + override val thumbnailSelector = ".gthumb" override val pageUri = "view" - override val pageSelector = ".gthumb" - private val serverSelector = "load_server" - - private fun serverNumber(document: Document, galleryId: String): String { - return document.inputIdValueOf(serverSelector).takeIf { - it.isNotBlank() - } ?: when (galleryId.toInt()) { - in 1..274825 -> "1" - in 274826..403818 -> "2" - in 403819..527143 -> "3" - in 527144..632481 -> "4" - in 632482..816010 -> "5" - in 816011..970098 -> "6" - in 970099..1121113 -> "7" - else -> "8" - } - } - - override fun getServer(document: Document, galleryId: String): String { - val domain = baseUrl.toHttpUrl().host - return "m${serverNumber(document, galleryId)}.$domain" - } - - override fun pageRequestForm(document: Document, totalPages: String): FormBody { - val galleryId = document.inputIdValueOf(galleryIdSelector) - - return FormBody.Builder() - .add("server", serverNumber(document, galleryId)) - .add("u_id", document.inputIdValueOf(galleryIdSelector)) - .add("g_id", document.inputIdValueOf(loadIdSelector)) - .add("img_dir", document.inputIdValueOf(loadDirSelector)) - .add("visible_pages", "10") - .add("total_pages", totalPages) - .add("type", "2") // 1 would be "more", 2 is "all remaining" - .build() - } - - /* Filters */ - override fun tagsParser(document: Document): List> { - return document.select(".stags .tag_btn") - .mapNotNull { - Pair( - it.selectFirst(".list_tag")?.ownText() ?: "", - it.select("a").attr("href") - .removeSuffix("/").substringAfterLast('/'), - ) - } - } - - override val idPrefixUri = "gallery" } diff --git a/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentaiUrlActivity.kt b/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentaiUrlActivity.kt deleted file mode 100644 index 4d76b0763..000000000 --- a/src/all/imhentai/src/eu/kanade/tachiyomi/extension/all/imhentai/IMHentaiUrlActivity.kt +++ /dev/null @@ -1,38 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.imhentai - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.util.Log -import kotlin.system.exitProcess - -/** - * Springboard that accepts https://imhentai.xxx/gallery/xxxxxx intents and redirects them to - * the main Tachiyomi process. - */ -class IMHentaiUrlActivity : Activity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val pathSegments = intent?.data?.pathSegments - if (pathSegments != null && pathSegments.size > 1) { - val id = pathSegments[1] - val mainIntent = Intent().apply { - action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "id:$id") - putExtra("filter", packageName) - } - - try { - startActivity(mainIntent) - } catch (e: ActivityNotFoundException) { - Log.e("IMHentaiUrlActivity", e.toString()) - } - } else { - Log.e("IMHentaiUrlActivity", "could not parse uri from intent $intent") - } - - finish() - exitProcess(0) - } -}