Update theme GalleryAdults (#2911)

* 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
This commit is contained in:
Cuong M. Tran 2024-05-12 11:36:48 +07:00 committed by Draff
parent 87157d8aa9
commit 6abded47de
9 changed files with 318 additions and 354 deletions

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 0
baseVersionCode = 1

View File

@ -55,14 +55,12 @@ abstract class GalleryAdults(
.add("X-Requested-With", "XMLHttpRequest")
.build()
/* Preferences */
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().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<MangasPage> {
val randomEntryFilter = filters.filterIsInstance<RandomEntryFilter>().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/<id>/
*/
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<SortOrderFilter>().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<SortOrderFilter>().firstOrNull()
val genresFilter = filters.filterIsInstance<GenresFilter>().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<AdvancedTextFilter>()
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<SortOrderFilter>().firstOrNull()
val genresFilter = filters.filterIsInstance<GenresFilter>().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<SortOrderFilter>().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<SChapter> {
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<Page> {
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/<id>/<page>
*/
protected open val pageUri = "g"
override fun pageListParse(document: Document): List<Page> {
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<Page>()
@ -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<Page> {
// 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<Genre>()
private fun tagsRequest(page: Int): Request {
/**
* List of tags in <name, uri> pairs
*/
protected var genres: MutableMap<String, String> = 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<Pair<String, String>> {
return document.select(".list_tags .tag_item")
/**
* Parsing [document] to return a list of tags in <name, uri> pairs.
*/
protected open fun tagsParser(document: Document): List<Genre> {
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<Pair<String, String>>()
val tags = mutableListOf<Genre>()
runBlocking {
val jobsPool = mutableListOf<Job>()
// 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<Filter<*>>().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)
}

View File

@ -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<Genre>) : Filter.Group<Genre>(
class GenresFilter(genres: Map<String, String>) : Filter.Group<Genre>(
"Tags",
genres.map { Genre(it.name, it.uri) },
genres.map { Genre(it.key, it.value) },
)
class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>) :
@ -13,6 +13,8 @@ class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>) :
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)

View File

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

View File

@ -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<Pair<String, String>> {
return document.select(".tags_page ul.tags li")
override fun tagsParser(document: Document): List<Genre> {
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,
)
}

View File

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

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.imhentai.IMHentaiUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="imhentai.xxx"
android:pathPattern="/gallery/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -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<Pair<String, String>> {
return document.select(".stags .tag_btn")
.mapNotNull {
Pair(
it.selectFirst(".list_tag")?.ownText() ?: "",
it.select("a").attr("href")
.removeSuffix("/").substringAfterLast('/'),
)
}
}
override val idPrefixUri = "gallery"
}

View File

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