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") id("lib-multisrc")
} }
baseVersionCode = 0 baseVersionCode = 1

View File

@ -55,14 +55,12 @@ abstract class GalleryAdults(
.add("X-Requested-With", "XMLHttpRequest") .add("X-Requested-With", "XMLHttpRequest")
.build() .build()
/* Preferences */
protected val preferences: SharedPreferences by lazy { protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
protected val SharedPreferences.shortTitle protected open val useShortTitlePreference = true
get() = getBoolean(PREF_SHORT_TITLE, false)
private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""")
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
@ -71,18 +69,12 @@ abstract class GalleryAdults(
summaryOff = "Showing Long Titles" summaryOff = "Showing Long Titles"
summaryOn = "Showing short Titles" summaryOn = "Showing short Titles"
setDefaultValue(false) setDefaultValue(false)
setVisible(useShortTitlePreference)
}.also(screen::addPreference) }.also(screen::addPreference)
} }
protected open fun Element.mangaTitle(selector: String = ".caption"): String? = protected val SharedPreferences.shortTitle
mangaFullTitle(selector).let { get() = getBoolean(PREF_SHORT_TITLE, false)
if (preferences.shortTitle) it?.shortenTitle() else it
}
protected fun Element.mangaFullTitle(selector: String) =
selectFirst(selector)?.text()
private fun String.shortenTitle() = this.replace(shortenTitleRegex, "").trim()
/* List detail */ /* List detail */
protected class SMangaDto( protected class SMangaDto(
@ -92,6 +84,18 @@ abstract class GalleryAdults(
val lang: String, 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() = protected open fun Element.mangaUrl() =
selectFirst(".inner_thumb a")?.attr("abs:href") selectFirst(".inner_thumb a")?.attr("abs:href")
@ -107,7 +111,7 @@ abstract class GalleryAdults(
if (!url.endsWith('/') && !url.contains('?')) { if (!url.endsWith('/') && !url.contains('?')) {
addPathSegment("") // trailing slash (/) addPathSegment("") // trailing slash (/)
} }
if (page > 1) addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
return this return this
} }
@ -150,7 +154,14 @@ abstract class GalleryAdults(
/* Search */ /* Search */
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val randomEntryFilter = filters.filterIsInstance<RandomEntryFilter>().firstOrNull()
return when { return when {
randomEntryFilter?.state == true -> {
client.newCall(randomEntryRequest())
.asObservableSuccess()
.map { response -> randomEntryParse(response) }
}
query.startsWith(PREFIX_ID_SEARCH) -> { query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH) val id = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(id)) 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 { protected open fun searchMangaByIdRequest(id: String): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply { val url = baseUrl.toHttpUrl().newBuilder().apply {
@ -189,7 +222,7 @@ abstract class GalleryAdults(
protected open val useIntermediateSearch: Boolean = false protected open val useIntermediateSearch: Boolean = false
protected open val supportAdvancedSearch: Boolean = false protected open val supportAdvancedSearch: Boolean = false
protected open val supportSpeechless: Boolean = false protected open val supportSpeechless: Boolean = false
private val useBasicSearch: Boolean protected open val useBasicSearch: Boolean
get() = !useIntermediateSearch get() = !useIntermediateSearch
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -225,52 +258,7 @@ abstract class GalleryAdults(
} }
} }
/** protected open val basicSearchKey = "q"
* 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)
}
/** /**
* Basic Search: support query string with multiple-genres filter by adding genres to query string. * 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 { val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegments("search/") addPathSegments("search/")
addEncodedQueryParameter("q", buildQueryString(selectedGenres.map { it.name }, query)) addEncodedQueryParameter(basicSearchKey, buildQueryString(selectedGenres.map { it.name }, query))
// Search results sorting is not supported by AsmHentai
if (sortOrderFilter?.state == 0) addQueryParameter("sort", "popular") if (sortOrderFilter?.state == 0) addQueryParameter("sort", "popular")
addPageUri(page) addPageUri(page)
} }
return GET(url.build(), headers) return GET(url.build(), headers)
} }
protected open val intermediateSearchKey = "key"
/** /**
* This supports filter query search with languages, categories (manga, doujinshi...) * This supports filter query search with languages, categories (manga, doujinshi...)
* with additional sort orders. * with additional sort orders.
@ -318,12 +307,15 @@ abstract class GalleryAdults(
toBinary(mangaLang == pair.first || mangaLang == LANGUAGE_MULTI), 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) addPageUri(page)
} }
return GET(url.build()) 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 * Advanced Search normally won't support search for string but allow include/exclude specific
* tags/artists/groups/parodies/characters * tags/artists/groups/parodies/characters
@ -339,7 +331,7 @@ abstract class GalleryAdults(
// Advanced search // Advanced search
val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>() val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>()
val url = "$baseUrl/advsearch".toHttpUrl().newBuilder().apply { val url = "$baseUrl/$advancedSearchUri".toHttpUrl().newBuilder().apply {
getSortOrderURIs().forEachIndexed { index, pair -> getSortOrderURIs().forEachIndexed { index, pair ->
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index)) addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
} }
@ -384,7 +376,7 @@ abstract class GalleryAdults(
) )
} }
} }
addEncodedQueryParameter("key", keys.joinToString("+")) addEncodedQueryParameter(advancedSearchKey, keys.joinToString("+"))
addPageUri(page) addPageUri(page)
} }
return GET(url.build()) 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 { protected open fun loginRequired(document: Document, url: String): Boolean {
return ( return (
@ -453,34 +492,7 @@ abstract class GalleryAdults(
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
/* Details */ /* 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 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 { override fun mangaDetailsParse(document: Document): SManga {
return document.selectFirst(mangaDetailInfoSelector)!!.run { return document.selectFirst(mangaDetailInfoSelector)!!.run {
@ -491,11 +503,54 @@ abstract class GalleryAdults(
thumbnail_url = getCover() thumbnail_url = getCover()
genre = getInfo("Tags") genre = getInfo("Tags")
author = getInfo("Artists") 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 */ /* Chapters */
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
@ -515,39 +570,71 @@ abstract class GalleryAdults(
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException() override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
/* Pages */ /* Pages */
protected open fun Document.inputIdValueOf(string: String): String { protected open fun Element.inputIdValueOf(string: String): String {
return select("input[id=$string]").attr("value") return select("input[id=$string]").attr("value")
} }
protected open val pagesRequest = "inc/thumbs_loader.php"
protected open val galleryIdSelector = "gallery_id" protected open val galleryIdSelector = "gallery_id"
protected open val loadIdSelector = "load_id" protected open val loadIdSelector = "load_id"
protected open val loadDirSelector = "load_dir" protected open val loadDirSelector = "load_dir"
protected open val totalPagesSelector = "load_pages" protected open val totalPagesSelector = "load_pages"
protected open val pageUri = "g" protected open val serverSelector = "load_server"
protected open val pageSelector = ".gallery_thumb"
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() private val jsonFormat: Json by injectLazy()
protected open fun getServer(document: Document, galleryId: String): String { protected open fun Element.getServer(): String {
val cover = document.getCover() val domain = baseUrl.toHttpUrl().host
return cover!!.toHttpUrl().host return serverNumber()
?.let { "m$it.$domain" }
?: getCover()!!.toHttpUrl().host
} }
override fun pageListParse(document: Document): List<Page> { protected open fun Element.serverNumber(): String? =
val json = document.selectFirst("script:containsData(parseJSON)")?.data() inputIdValueOf(serverSelector)
.takeIf { it.isNotBlank() }
protected open fun Element.parseJson(): String? =
selectFirst("script:containsData(parseJSON)")?.data()
?.substringAfter("$.parseJSON('") ?.substringAfter("$.parseJSON('")
?.substringBefore("');")?.trim() ?.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) { if (json != null) {
val loadDir = document.inputIdValueOf(loadDirSelector) val loadDir = document.inputIdValueOf(loadDirSelector)
val loadId = document.inputIdValueOf(loadIdSelector) val loadId = document.inputIdValueOf(loadIdSelector)
val galleryId = document.inputIdValueOf(galleryIdSelector) val galleryId = document.inputIdValueOf(galleryIdSelector)
val pageUrl = "$baseUrl/$pageUri/$galleryId" val pageUrl = "$baseUrl/$pageUri/$galleryId"
val randomServer = getServer(document, galleryId) val server = document.getServer()
val imagesUri = "https://$randomServer/$loadDir/$loadId" val imagesUri = "https://$server/$loadDir/$loadId"
try { try {
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()
@ -583,9 +670,11 @@ abstract class GalleryAdults(
/** /**
* Overwrite this to force extension not blindly converting thumbnails to full image * 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. * one by one, then parsing for actual image's URL.
* This will be much slower but guaranteed work. * This will be much slower but guaranteed work.
*
* This only apply if site doesn't provide 'parseJSON'.
*/ */
protected open val parsingImagePageByPage: Boolean = false 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]. * 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> { 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 totalPages = document.inputIdValueOf(totalPagesSelector)
val galleryId = document.inputIdValueOf(galleryIdSelector) val galleryId = document.inputIdValueOf(galleryIdSelector)
val pageUrl = "$baseUrl/$pageUri/$galleryId" val pageUrl = "$baseUrl/$pageUri/$galleryId"
val pages = document.select("$pageSelector a") val pages = document.select("$thumbnailSelector a")
.map { .map {
if (parsingImagePageByPage) { if (parsingImagePageByPage) {
it.absUrl("href") it.absUrl("href")
@ -611,8 +699,8 @@ abstract class GalleryAdults(
} }
.toMutableList() .toMutableList()
if (totalPages.isNotBlank()) { if (totalPages.isNotBlank() && totalPages.toInt() > pages.size) {
val form = pageRequestForm(document, totalPages) val form = pageRequestForm(document, totalPages, pages.size)
val morePages = client.newCall(POST("$baseUrl/$pagesRequest", xhrHeaders, form)) val morePages = client.newCall(POST("$baseUrl/$pagesRequest", xhrHeaders, form))
.execute() .execute()
@ -655,16 +743,15 @@ abstract class GalleryAdults(
val galleryId = document.inputIdValueOf(galleryIdSelector) val galleryId = document.inputIdValueOf(galleryIdSelector)
val pageUrl = "$baseUrl/$pageUri/$galleryId" val pageUrl = "$baseUrl/$pageUri/$galleryId"
val randomServer = getServer(document, galleryId) val server = document.getServer()
val imagesUri = "https://$randomServer/$loadDir/$loadId" 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() 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) val totalPages = document.inputIdValueOf(totalPagesSelector)
if (totalPages.isNotBlank()) { if (totalPages.isNotBlank() && totalPages.toInt() > thumbUrls.size) {
val imagesExt = images.first()?.imgAttr()!! val imagesExt = images.first()?.imgAttr()!!
.substringAfterLast('.') .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 { override fun imageUrlParse(document: Document): String {
return document.selectFirst("img#gimg, img#fimg")?.imgAttr()!! return document.selectFirst("img#gimg, img#fimg")?.imgAttr()!!
} }
@ -700,10 +777,15 @@ abstract class GalleryAdults(
/* Filters */ /* Filters */
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
private fun launchIO(block: () -> Unit) = scope.launch { block() } private fun launchIO(block: () -> Unit) = scope.launch { block() }
private var tagsFetched = false
private var tagsFetchAttempt = 0 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 { val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegments("tags/popular") addPathSegments("tags/popular")
addPageUri(page) addPageUri(page)
@ -711,21 +793,24 @@ abstract class GalleryAdults(
return GET(url.build(), headers) 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 { .mapNotNull {
Pair( Genre(
it.selectFirst("h3.list_tag")?.ownText() ?: "", it.select(".list_tag, .tag_name").text(),
it.select("a").attr("href") it.attr("href")
.removeSuffix("/").substringAfterLast('/'), .removeSuffix("/").substringAfterLast('/'),
) )
} }
} }
private fun getGenres() { protected open fun requestTags() {
if (genres.isEmpty() && tagsFetchAttempt < 3) { if (!tagsFetched && tagsFetchAttempt < 3) {
launchIO { launchIO {
val tags = mutableListOf<Pair<String, String>>() val tags = mutableListOf<Genre>()
runBlocking { runBlocking {
val jobsPool = mutableListOf<Job>() val jobsPool = mutableListOf<Job>()
// Get first 3 pages // Get first 3 pages
@ -742,7 +827,11 @@ abstract class GalleryAdults(
) )
} }
jobsPool.joinAll() 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++ tagsFetchAttempt++
@ -751,7 +840,7 @@ abstract class GalleryAdults(
} }
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
getGenres() requestTags()
val filters = emptyList<Filter<*>>().toMutableList() val filters = emptyList<Filter<*>>().toMutableList()
if (useIntermediateSearch) { if (useIntermediateSearch) {
filters.add(Filter.Header("HINT: Separate search term with comma (,)")) filters.add(Filter.Header("HINT: Separate search term with comma (,)"))
@ -765,7 +854,7 @@ abstract class GalleryAdults(
filters.add(GenresFilter(genres)) filters.add(GenresFilter(genres))
} }
if (useIntermediateSearch) { if (useIntermediateSearch || supportAdvancedSearch) {
filters.addAll( filters.addAll(
listOf( listOf(
Filter.Separator(), Filter.Separator(),
@ -795,6 +884,8 @@ abstract class GalleryAdults(
} }
filters.add(FavoriteFilter()) filters.add(FavoriteFilter())
filters.add(RandomEntryFilter())
return FilterList(filters) return FilterList(filters)
} }

View File

@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.multisrc.galleryadults
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
class Genre(name: String, val uri: String) : Filter.CheckBox(name) 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", "Tags",
genres.map { Genre(it.name, it.uri) }, genres.map { Genre(it.key, it.value) },
) )
class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>) : 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 FavoriteFilter : Filter.CheckBox("Show favorites only (login via WebView)", false)
class RandomEntryFilter : Filter.CheckBox("Random manga", false)
// Speechless // Speechless
class SpeechlessFilter : Filter.CheckBox("Show speechless items only", false) 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("data-lazy-src") -> absUrl("data-lazy-src")
hasAttr("srcset") -> absUrl("srcset").substringBefore(" ") hasAttr("srcset") -> absUrl("srcset").substringBefore(" ")
else -> absUrl("src") else -> absUrl("src")
}!! }
fun Element.cleanTag(): String = text().cleanTag() fun Element.cleanTag(): String = text().cleanTag()
fun String.cleanTag(): String = replace(regexTagCountNumber, "").trim() fun String.cleanTag(): String = replace(regexTagCountNumber, "").trim()
@ -57,7 +57,7 @@ fun String?.toDate(simpleDateFormat: SimpleDateFormat?): Long {
} }
private fun parseDate(date: String?): Long { private fun parseDate(date: String?): Long {
date ?: return 0 date ?: return 0L
return when { return when {
// Handle 'yesterday' and 'today', using midnight // Handle 'yesterday' and 'today', using midnight

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.extension.all.asmhentai package eu.kanade.tachiyomi.extension.all.asmhentai
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults 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.multisrc.galleryadults.imgAttr
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -31,64 +31,64 @@ class AsmHentai(
override fun popularMangaSelector() = ".preview_item" 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 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 mangaDetailInfoSelector = ".book_page"
override val galleryIdSelector = "load_id" /**
* [totalPagesSelector] only exists if pages > 10
*/
override val totalPagesSelector = "t_pages" 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") val token = document.select("[name=csrf-token]").attr("content")
return FormBody.Builder() return FormBody.Builder()
.add("_token", token)
.add("id", document.inputIdValueOf(loadIdSelector)) .add("id", document.inputIdValueOf(loadIdSelector))
.add("dir", document.inputIdValueOf(loadDirSelector)) .add("dir", document.inputIdValueOf(loadDirSelector))
.add("visible_pages", "10") .add("visible_pages", loadedPages.toString())
.add("t_pages", totalPages) .add("t_pages", totalPages)
.add("type", "2") // 1 would be "more", 2 is "all remaining" .add("type", "2") // 1 would be "more", 2 is "all remaining"
.apply {
if (token.isNotBlank()) add("_token", token)
}
.build() .build()
} }
/* Filters */ override fun tagsParser(document: Document): List<Genre> {
override fun tagsParser(document: Document): List<Pair<String, String>> { return document.select(".tags_page .tags a.tag")
return document.select(".tags_page ul.tags li")
.mapNotNull { .mapNotNull {
Pair( Genre(
it.selectFirst("a.tag")?.ownText() ?: "", it.ownText(),
it.select("a.tag").attr("href") it.attr("href")
.removeSuffix("/").substringAfterLast('/'), .removeSuffix("/").substringAfterLast('/'),
) )
} }
@ -97,6 +97,7 @@ class AsmHentai(
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
listOf( listOf(
Filter.Header("HINT: Separate search term with comma (,)"), Filter.Header("HINT: Separate search term with comma (,)"),
Filter.Header("String query search doesn't support Sort"),
) + super.getFilterList().list, ) + 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.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import kotlin.random.Random
class HentaiFox( class HentaiFox(
lang: String = "all", lang: String = "all",
@ -17,8 +14,6 @@ class HentaiFox(
"HentaiFox", "HentaiFox",
"https://hentaifox.com", "https://hentaifox.com",
lang = lang, lang = lang,
mangaLang = mangaLang,
simpleDateFormat = null,
) { ) {
override val supportsLatest = mangaLang.isNotBlank() 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.mangaTitle(selector: String): String? = mangaFullTitle(selector)
override fun Element.getTime(): Long { override fun Element.getInfo(tag: String): String {
return selectFirst(".pages:contains(Posted:)")?.ownText() 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: ") ?.removePrefix("Posted: ")
.toDate(simpleDateFormat) .toDate(simpleDateFormat)
}
override fun HttpUrl.Builder.addPageUri(page: Int): HttpUrl.Builder { override fun HttpUrl.Builder.addPageUri(page: Int): HttpUrl.Builder {
val url = toString() val url = toString()
@ -57,22 +71,13 @@ class HentaiFox(
addPathSegments("page/$page") addPathSegments("page/$page")
url.contains('?') -> url.contains('?') ->
addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
page > 1 -> else ->
addPathSegments("pag/$page") addPathSegments("pag/$page")
} }
addPathSegment("") // trailing slash (/) addPathSegment("") // trailing slash (/)
return this 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: * 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') * - 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( override fun getFilterList() = FilterList(
listOf( listOf(
Filter.Header("HINT: Use double quote (\") for exact match"), Filter.Header("HINT: Use double quote (\") for exact match"),
) + super.getFilterList().list, ) + 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 package eu.kanade.tachiyomi.extension.all.imhentai
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.multisrc.galleryadults.cleanTag
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.io.IOException import java.io.IOException
@ -61,46 +57,24 @@ class IMHentai(
}, },
).build() ).build()
override val favoritePath = "user/fav_pags.php"
/* Details */ /* Details */
override fun Element.getInfo(tag: String): String { override fun Element.getInfo(tag: String): String {
return select("li:has(.tags_text:contains($tag:)) .tag").map { return select("li:has(.tags_text:contains($tag:)) a.tag")
it?.run { .joinToString {
val name = it.ownText()
if (tag.contains(regexTag)) {
genres[name] = it.attr("href")
.removeSuffix("/").substringAfterLast('/')
}
listOf( listOf(
ownText().cleanTag(), name,
select(".split_tag").text() it.select(".split_tag").text()
.trim() .trim()
.removePrefix("| ") .removePrefix("| "),
.cleanTag(),
) )
.filter { s -> s.isNotBlank() } .filter { s -> s.isNotBlank() }
.joinToString() .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() = override fun Element.getCover() =
@ -109,55 +83,6 @@ class IMHentai(
override val mangaDetailInfoSelector = ".gallery_first" override val mangaDetailInfoSelector = ".gallery_first"
/* Pages */ /* Pages */
override val thumbnailSelector = ".gthumb"
override val pageUri = "view" 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)
}
}