diff --git a/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/FoolSlide.kt b/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/FoolSlide.kt index b311d5bb2..ba1fed725 100644 --- a/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/FoolSlide.kt +++ b/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/FoolSlide.kt @@ -18,6 +18,8 @@ import java.util.* open class FoolSlide(override val name: String, override val baseUrl: String, override val lang: String, val urlModifier: String = "") : ParsedHttpSource() { + protected open val dedupeLatestUpdates = true + override val supportsLatest = true override fun popularMangaSelector() = "div.group" @@ -30,9 +32,11 @@ open class FoolSlide(override val name: String, override val baseUrl: String, ov override fun latestUpdatesParse(response: Response): MangasPage { val mp = super.latestUpdatesParse(response) - val mangas = mp.mangas.distinctBy { it.url }.filterNot { latestUpdatesUrls.contains(it.url) } - latestUpdatesUrls.addAll(mangas.map { it.url }) - return MangasPage(mangas, mp.hasNextPage) + return if(dedupeLatestUpdates) { + val mangas = mp.mangas.distinctBy { it.url }.filterNot { latestUpdatesUrls.contains(it.url) } + latestUpdatesUrls.addAll(mangas.map { it.url }) + MangasPage(mangas, mp.hasNextPage) + } else mp } override fun latestUpdatesSelector() = "div.group" @@ -113,8 +117,10 @@ open class FoolSlide(override val name: String, override val baseUrl: String, ov /** * Transform a GET request into a POST request that automatically authorizes all adult content */ - private fun allowAdult(request: Request): Request { - return POST(request.url().toString(), body = FormBody.Builder() + private fun allowAdult(request: Request) = allowAdult(request.url().toString()) + + protected fun allowAdult(url: String): Request { + return POST(url, body = FormBody.Builder() .add("adult", "true") .build()) } diff --git a/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/FoolSlideFactory.kt b/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/FoolSlideFactory.kt index e025b4639..1c44a7f0a 100644 --- a/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/FoolSlideFactory.kt +++ b/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/FoolSlideFactory.kt @@ -6,8 +6,7 @@ import com.google.gson.JsonParser import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceFactory -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.* import okhttp3.Request import org.jsoup.nodes.Document @@ -50,7 +49,8 @@ fun getAllFoolSlide(): List { EvilFlowers(), AkaiYuhiMunTeam(), LupiTeam(), - HotChocolateScans() + HotChocolateScans(), + HentaiCafe() ) } @@ -201,3 +201,4 @@ class LupiTeam : FoolSlide("LupiTeam", "https://lupiteam.net", "it", "/reader") return manga } } + diff --git a/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/HentaiCafe.kt b/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/HentaiCafe.kt new file mode 100644 index 000000000..d0a0e4076 --- /dev/null +++ b/src/all/foolslide/src/eu/kanade/tachiyomi/extension/en/foolslide/HentaiCafe.kt @@ -0,0 +1,192 @@ +package eu.kanade.tachiyomi.extension.all.foolslide + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservable +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import java.net.URLEncoder + +class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga") { + // We have custom latest updates logic so do not dedupe latest updates + override val dedupeLatestUpdates = false + + // Does not support popular manga + override fun fetchPopularManga(page: Int) = fetchLatestUpdates(page) + + override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { + val urlElement = element.select(".entry-thumb").first() + setUrlWithoutDomain(urlElement.attr("href")) + thumbnail_url = urlElement.child(0).attr("src") + title = element.select(".entry-title").text().trim() + } + + override fun latestUpdatesNextPageSelector() = ".x-pagination li:last-child a" + + override fun latestUpdatesRequest(page: Int) = pagedRequest("$baseUrl/", page) + + override fun latestUpdatesSelector() = "article" + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + title = document.select(".entry-title").text() + val contentElement = document.select(".entry-content").first() + thumbnail_url = contentElement.child(0).child(0).attr("src") + + fun filterableTagsOfType(type: String) = contentElement.select("a") + .filter { "$baseUrl/$type/" in it.attr("href") } + .joinToString { it.text() } + + genre = filterableTagsOfType("tag") + artist = filterableTagsOfType("artist") + } + + // Note that the reader URL cannot be deduced from the manga URL all the time which is why + // we still need to parse the manga info page + // Example: https://hentai.cafe/aiya-youngest-daughters-circumstances/ + override fun chapterListParse(response: Response) = listOf( + SChapter.create().apply { + setUrlWithoutDomain(response.asJsoup().select("[title=Read]").attr("href")) + name = "Chapter" + chapter_number = 0.0f + } + ) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + var url: String? = null + var queryString: String? = null + fun requireNoUrl() = require(url == null && queryString == null) { + "You cannot combine filters or use text search with filters!" + } + + filters.findInstance()?.let { f -> + if(f.state.isNotBlank()) { + requireNoUrl() + url = "/artist/${f.state + .trim() + .toLowerCase() + .replace(ARTIST_INVALID_CHAR_REGEX, "-")}/" + } + } + filters.findInstance()?.let { f -> + if(f.state) { + requireNoUrl() + url = "/category/book/" + } + } + filters.findInstance()?.let { f -> + if(f.state != 0) { + requireNoUrl() + url = "/tag/${f.values[f.state].name}/" + } + } + + if(query.isNotBlank()) { + requireNoUrl() + url = "/" + queryString = "s=" + URLEncoder.encode(query, "UTF-8") + } + + return url?.let { + pagedRequest("$baseUrl$url", page, queryString) + } ?: latestUpdatesRequest(page) + } + + private fun pagedRequest(url: String, page: Int, queryString: String? = null): Request { + // The site redirects page 1 -> url-without-page so we do this redirect early for optimization + val builtUrl = if(page == 1) url else "${url}page/$page/" + return GET(if(queryString != null) "$builtUrl?$queryString" else builtUrl) + } + + override fun searchMangaParse(response: Response) = latestUpdatesParse(response) + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return client.newCall(searchMangaRequest(page, query, filters)) + .asObservable().doOnNext { response -> + if(!response.isSuccessful) { + response.close() + // Better error message for invalid artist + if (response.code() == 404 + && !filters.findInstance()?.state.isNullOrBlank()) + error("Invalid artist!") + else throw Exception("HTTP error ${response.code()}") + } + } + .map { response -> + searchMangaParse(response) + } + } + + override fun getFilterList() = FilterList( + Filter.Header("Filters cannot be used while searching."), + Filter.Header("Only one filter may be used at a time."), + Filter.Separator(), + ArtistFilter(), + BookFilter(), + TagFilter() + ) + + class ArtistFilter : Filter.Text("Artist (must be exact match)") + class BookFilter : Filter.CheckBox("Show books only", false) + class TagFilter : Filter.Select("Tag", arrayOf( + Tag("all", "All"), + Tag("ahegao", "Ahegao"), + Tag("anal", "Anal"), + Tag("big-ass", "Big ass"), + Tag("big-breast", "Big breast"), + Tag("big-dick", "Big dick"), + Tag("bondage", "Bondage"), + Tag("cheating", "Cheating"), + Tag("chubby", "Chubby"), + Tag("color", "Color"), + Tag("condom", "Condom"), + Tag("cosplay", "Cosplay"), + Tag("cunnilingus", "Cunnilingus"), + Tag("dark-skin", "Dark skin"), + Tag("exhibitionism", "Exhibitionism"), + Tag("fellatio", "Fellatio"), + Tag("femdom", "Femdom"), + Tag("flat-chest", "Flat chest"), + Tag("full-color", "Full color"), + Tag("glasses", "Glasses"), + Tag("group", "Group"), + Tag("hairy", "Hairy"), + Tag("handjob", "Handjob"), + Tag("housewife", "Housewife"), + Tag("incest", "Incest"), + Tag("large-breast", "Large breast"), + Tag("lingerie", "Lingerie"), + Tag("loli", "Loli"), + Tag("masturbation", "Masturbation"), + Tag("nakadashi", "Nakadashi"), + Tag("osananajimi", "Osananajimi"), + Tag("paizuri", "Paizuri"), + Tag("pettanko", "Pettanko"), + Tag("rape", "Rape"), + Tag("schoolgirl", "Schoolgirl"), + Tag("sex-toys", "Sex Toys"), + Tag("shota", "Shota"), + Tag("socks", "Socks"), + Tag("stocking", "Stocking"), + Tag("stockings", "Stockings"), + Tag("swimsuit", "Swimsuit"), + Tag("teacher", "Teacher"), + Tag("tsundere", "Tsundere"), + Tag("uncensored", "uncensored"), + Tag("vanilla", "Vanilla"), + Tag("x-ray", "X-ray") + )) + class Tag(val name: String, val displayName: String) { + override fun toString() = displayName + } + + companion object { + // Do not include dashes in this regex, this way we can deduplicate dashes + private val ARTIST_INVALID_CHAR_REGEX = Regex("[^a-zA-Z0-9]+") + } +} + +private inline fun Iterable<*>.findInstance() = find { it is T } as? T \ No newline at end of file