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:
parent
87157d8aa9
commit
6abded47de
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 0
|
baseVersionCode = 1
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue