HentaiRead | Fixed No Results Found, Added Filters (#5127)
* Fixed HentaiRead, Added Filters * Apply suggestion
This commit is contained in:
parent
1eb5b7e347
commit
e145bdb6bc
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Hentairead'
|
extClass = '.Hentairead'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://hentairead.com'
|
baseUrl = 'https://hentairead.com'
|
||||||
overrideVersionCode = 6
|
overrideVersionCode = 7
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.hentairead
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Results(
|
||||||
|
val results: List<Result>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Result(
|
||||||
|
val id: Int,
|
||||||
|
val text: String,
|
||||||
|
)
|
|
@ -0,0 +1,58 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.hentairead
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter.Sort.Selection
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
|
||||||
|
fun getFilters(): FilterList {
|
||||||
|
return FilterList(
|
||||||
|
SortFilter("Sort by", Selection(0, false), getSortsList),
|
||||||
|
TypeFilter("Types"),
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header("Separate tags with commas (,)"),
|
||||||
|
Filter.Header("Prepend with dash (-) to exclude [ Only for 'Tags' ]"),
|
||||||
|
TextFilter("Tags", "manga_tag"),
|
||||||
|
Filter.Separator(),
|
||||||
|
TextFilter("Artists", "artist"),
|
||||||
|
TextFilter("Circles", "circle"),
|
||||||
|
TextFilter("Characters", "character"),
|
||||||
|
TextFilter("Collections", "collection"),
|
||||||
|
TextFilter("Scanlators", "scanlator"),
|
||||||
|
TextFilter("Conventions", "convention"),
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header("Filter by year uploaded, for example: (>2024)"),
|
||||||
|
UploadedFilter("Uploaded"),
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header("Filter by pages, for example: (>20)"),
|
||||||
|
PageFilter("Pages"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal open class UploadedFilter(name: String) : Filter.Text(name)
|
||||||
|
|
||||||
|
internal open class PageFilter(name: String) : Filter.Text(name)
|
||||||
|
|
||||||
|
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
|
||||||
|
|
||||||
|
internal class TypeFilter(name: String) :
|
||||||
|
Filter.Group<CheckBoxFilter>(
|
||||||
|
name,
|
||||||
|
listOf(
|
||||||
|
"Doujinshi" to "4",
|
||||||
|
"Manga" to "52",
|
||||||
|
"Artist CG" to "4798",
|
||||||
|
).map { CheckBoxFilter(it.first, it.second, true) },
|
||||||
|
)
|
||||||
|
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
|
||||||
|
|
||||||
|
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
|
||||||
|
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
|
||||||
|
fun getValue() = vals[state!!.index].second
|
||||||
|
}
|
||||||
|
|
||||||
|
private val getSortsList: List<Pair<String, String>> = listOf(
|
||||||
|
Pair("Latest", "new"),
|
||||||
|
Pair("A-Z", "alphabet"),
|
||||||
|
Pair("Rating", "rating"),
|
||||||
|
Pair("Views", "views"),
|
||||||
|
)
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.en.hentairead
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
@ -11,6 +12,7 @@ import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
@ -37,9 +39,48 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override val mangaSubString = "hentai"
|
override val mangaSubString = "hentai"
|
||||||
override val fetchGenres = false
|
override fun popularMangaNextPageSelector(): String? = "a[rel=next]"
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
fun String.capitalizeEach() = this.split(" ").joinToString(" ") { s ->
|
||||||
|
s.replaceFirstChar { sr ->
|
||||||
|
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SManga.create().apply {
|
||||||
|
val authors = document.select("a[href*=/circle/] span:first-of-type").eachText().joinToString()
|
||||||
|
val artists = document.select("a[href*=/artist/] span:first-of-type").eachText().joinToString()
|
||||||
|
initialized = true
|
||||||
|
author = authors.ifEmpty { artists }
|
||||||
|
artist = artists.ifEmpty { authors }
|
||||||
|
genre = document.select("a[href*=/tag/] span:first-of-type").eachText().joinToString()
|
||||||
|
|
||||||
override fun getFilterList() = FilterList()
|
description = buildString {
|
||||||
|
document.select("a[href*=/characters/] span:first-of-type").eachText().joinToString().ifEmpty { null }?.let {
|
||||||
|
append("Characters: ", it.capitalizeEach(), "\n\n")
|
||||||
|
}
|
||||||
|
document.select("a[href*=/parody/] span:first-of-type").eachText().joinToString().ifEmpty { null }?.let {
|
||||||
|
append("Parodies: ", it.capitalizeEach(), "\n\n")
|
||||||
|
}
|
||||||
|
document.select("a[href*=/circle/] span:first-of-type").eachText().joinToString().ifEmpty { null }?.let {
|
||||||
|
append("Circles: ", it.capitalizeEach(), "\n\n")
|
||||||
|
}
|
||||||
|
document.select("a[href*=/convention/] span:first-of-type").eachText().joinToString().ifEmpty { null }?.let {
|
||||||
|
append("Convention: ", it.capitalizeEach(), "\n\n")
|
||||||
|
}
|
||||||
|
document.select("a[href*=/scanlator/] span:first-of-type").eachText().joinToString().ifEmpty { null }?.let {
|
||||||
|
append("Scanlators: ", it.capitalizeEach(), "\n\n")
|
||||||
|
}
|
||||||
|
document.selectFirst(".manga-titles h2")?.text()?.ifEmpty { null }?.let {
|
||||||
|
val titles = it.split("|").joinToString("\n") { "- ${it.trim()}" }
|
||||||
|
append("Alternative Titles: ", "\n", titles, "\n\n")
|
||||||
|
}
|
||||||
|
append(document.select(".items-center:contains(pages:)").text(), "\n")
|
||||||
|
}
|
||||||
|
status = SManga.COMPLETED
|
||||||
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun getFilterList(): FilterList = getFilters()
|
||||||
|
|
||||||
override fun searchLoadMoreRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchLoadMoreRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = "$baseUrl${searchPage(page)}".toHttpUrl().newBuilder()
|
val url = "$baseUrl${searchPage(page)}".toHttpUrl().newBuilder()
|
||||||
|
@ -50,20 +91,97 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
|
||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.c-tabs-item div.page-item-detail"
|
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/$mangaSubString/${searchPage(page)}?sortby=views", headers)
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/$mangaSubString/${searchPage(page)}?sortby=new", headers)
|
||||||
|
override fun popularMangaSelector() = ".manga-item"
|
||||||
|
override val popularMangaUrlSelector = ".manga-item__bottom a"
|
||||||
|
|
||||||
override val mangaDetailsSelectorDescription = "div.post-sub-title.alt-title > h2"
|
private fun getTagId(tag: String, type: String): Int? {
|
||||||
override val mangaDetailsSelectorAuthor = "div.post-meta.post-tax-wp-manga-artist > span.post-tags > a > span.tag-name"
|
val ajax = "$baseUrl/wp-admin/admin-ajax.php?action=search_manga_terms&search=$tag&taxonomy=$type".replace("artist", "manga_artist")
|
||||||
override val mangaDetailsSelectorArtist = "div.post-meta.post-tax-wp-manga-artist > span.post-tags > a > span.tag-name"
|
val res = client.newCall(GET(ajax, headers)).execute()
|
||||||
override val mangaDetailsSelectorGenre = "div.post-meta.post-tax-wp-manga-genre > span.post-tags > a > span.tag-name"
|
val items = res.parseAs<Results>()
|
||||||
override val mangaDetailsSelectorTag = "div.post-meta.post-tax-wp-manga-tag > span.post-tags > a > span.tag-name"
|
val item = items.results.filter { it.text.lowercase() == tag.lowercase() }
|
||||||
|
if (item.isNotEmpty()) {
|
||||||
|
return item[0].id
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
addPathSegments("page/$page")
|
||||||
|
addQueryParameter("s", query)
|
||||||
|
addQueryParameter("title-type", "contains")
|
||||||
|
filters.forEach {
|
||||||
|
when (it) {
|
||||||
|
is TypeFilter -> {
|
||||||
|
val (activeFilter, inactiveFilters) = it.state.partition { stIt -> stIt.state }
|
||||||
|
activeFilter.map { fil -> addQueryParameter("categories[]", fil.value) }
|
||||||
|
}
|
||||||
|
|
||||||
override val pageListParseSelector = "li.chapter-image-item > a > div.image-wrapper"
|
is PageFilter -> {
|
||||||
|
if (it.state.isNotBlank()) {
|
||||||
|
val (min, max) = parsePageRange(it.state)
|
||||||
|
addQueryParameter("pages", "$min-$max")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
is UploadedFilter -> {
|
||||||
return super.mangaDetailsParse(document).apply {
|
if (it.state.isNotBlank()) {
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
val type = when (it.state.firstOrNull()) {
|
||||||
status = SManga.COMPLETED
|
'>' -> "after"
|
||||||
|
'<' -> "before"
|
||||||
|
else -> "in"
|
||||||
|
}
|
||||||
|
addQueryParameter("release-type", type)
|
||||||
|
addQueryParameter("release", it.state.filter(Char::isDigit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is TextFilter -> {
|
||||||
|
if (it.state.isNotEmpty()) {
|
||||||
|
it.state.split(",").filter(String::isNotBlank).map { tag ->
|
||||||
|
val trimmed = tag.trim()
|
||||||
|
val id = getTagId(trimmed.removePrefix("-"), it.type)?.toString()
|
||||||
|
?: throw Exception("${it.type.lowercase().replaceFirstChar(Char::uppercase)} not found: ${trimmed.removePrefix("-")}")
|
||||||
|
if (it.type == "manga_tag") {
|
||||||
|
if (trimmed.startsWith('-')) {
|
||||||
|
addQueryParameter("excluding[]", id)
|
||||||
|
} else {
|
||||||
|
addQueryParameter("including[]", id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addQueryParameter("${it.type}s[]", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is SortFilter -> {
|
||||||
|
addQueryParameter("sortby", it.getValue())
|
||||||
|
addQueryParameter("order", if (it.state!!.ascending) "asc" else "desc")
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
|
||||||
|
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
|
||||||
|
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
|
||||||
|
|
||||||
|
if (num < 0) return minPages to maxPages
|
||||||
|
return when (query.firstOrNull()) {
|
||||||
|
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
|
||||||
|
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
|
||||||
|
'=' -> when (query[1]) {
|
||||||
|
'>' -> limitedNum() to maxPages
|
||||||
|
'<' -> 1 to limitedNum(maxPages)
|
||||||
|
else -> limitedNum() to limitedNum()
|
||||||
|
}
|
||||||
|
else -> limitedNum() to limitedNum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,10 +189,10 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
launchIO { countViews(document) }
|
launchIO { countViews(document) }
|
||||||
|
|
||||||
val pages = document.selectFirst("#chapter_preloaded_images")?.data()
|
val pages = document.selectFirst("[id=single-chapter-js-extra]")?.data()
|
||||||
?.substringAfter("chapter_preloaded_images = ")
|
?.substringAfter(":[")
|
||||||
?.substringBefore("],")
|
?.substringBefore("],")
|
||||||
?.let { json.decodeFromString<List<PageDto>>("$it]") }
|
?.let { json.decodeFromString<List<PageDto>>("[$it]") }
|
||||||
?: throw Exception("Failed to find page list. Non-English entries are not supported.")
|
?: throw Exception("Failed to find page list. Non-English entries are not supported.")
|
||||||
|
|
||||||
return pages.mapIndexed { idx, page ->
|
return pages.mapIndexed { idx, page ->
|
||||||
|
@ -88,6 +206,9 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
name = "Chapter"
|
name = "Chapter"
|
||||||
url = manga.url
|
url = manga.url
|
||||||
|
if (manga.description?.contains("Scanlators") == true) {
|
||||||
|
scanlator = manga.description?.substringAfter("Scanlators: ")?.substringBefore("\n")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -112,6 +233,10 @@ class Hentairead : Madara("HentaiRead", "https://hentairead.com", "en", dateForm
|
||||||
else -> element.attr("abs:src")
|
else -> element.attr("abs:src")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
|
return json.decodeFromString(body.string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
Loading…
Reference in New Issue