Split FoolSlide Extension (#5840)

* Split FoolSlide Extension

* remove FoolSlide

* add className

* change default_res 

#5845

* add nsfw

* nsfw2
This commit is contained in:
Riztard Lanthorn
2021-02-15 22:36:28 +07:00
committed by GitHub
parent d2cdca33e2
commit a49001e314
43 changed files with 362 additions and 291 deletions

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'FoolSlide (multiple sources)'
pkgNameSuffix = 'all.foolslide'
extClass = '.FoolSlideFactory'
extVersionCode = 59
libVersion = '1.2'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,301 +0,0 @@
package eu.kanade.tachiyomi.extension.all.foolslide
import com.github.salomonbrys.kotson.get
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.HashSet
import java.util.Locale
abstract 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"
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl$urlModifier/directory/$page/", headers)
}
val latestUpdatesUrls = HashSet<String>()
override fun latestUpdatesParse(response: Response): MangasPage {
val mp = super.latestUpdatesParse(response)
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"
override fun latestUpdatesRequest(page: Int): Request {
if (page == 1) {
latestUpdatesUrls.clear()
}
return GET("$baseUrl$urlModifier/latest/$page/")
}
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a[title]").first().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
}
element.select("img").first()?.let {
manga.thumbnail_url = it.absUrl("src").replace("/thumb_", "/")
}
return manga
}
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a[title]").first().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
}
return manga
}
override fun popularMangaNextPageSelector() = "div.next"
override fun latestUpdatesNextPageSelector(): String? = "div.next"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val searchHeaders = headersBuilder().add("Content-Type", "application/x-www-form-urlencoded").build()
val form = FormBody.Builder()
.add("search", query)
return POST("$baseUrl$urlModifier/search/", searchHeaders, form.build())
}
override fun searchMangaSelector() = "div.group"
override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a[title]").first().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
}
return manga
}
override fun searchMangaNextPageSelector() = "a:has(span.next)"
override fun mangaDetailsRequest(manga: SManga) = allowAdult(super.mangaDetailsRequest(manga))
open val mangaDetailsInfoSelector = "div.info"
// if there's no image on the details page, get the first page of the first chapter
fun getDetailsThumbnail(document: Document, urlSelector: String = chapterUrlSelector): String? {
return document.select("div.thumbnail img, table.thumb img").firstOrNull()?.attr("abs:src")
?: document.select(chapterListSelector()).last().select(urlSelector).attr("abs:href")
.let { url -> client.newCall(allowAdult(GET(url, headers))).execute() }
.let { response -> pageListParse(response).first().imageUrl }
}
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select(mangaDetailsInfoSelector).firstOrNull()?.html()?.let { infoHtml ->
author = Regex("""(?i)(Author|Autore)</b>:\s?([^\n<]*)[\n<]""").find(infoHtml)?.groupValues?.get(2)
artist = Regex("""Artist</b>:\s?([^\n<]*)[\n<]""").find(infoHtml)?.groupValues?.get(1)
description = Regex("""(?i)(Synopsis|Description|Trama)</b>:\s?([^\n<]*)[\n<]""").find(infoHtml)?.groupValues?.get(2)
}
thumbnail_url = getDetailsThumbnail(document)
}
}
/**
* Transform a GET request into a POST request that automatically authorizes all adult content
*/
private fun allowAdult(request: Request) = allowAdult(request.url().toString())
private fun allowAdult(url: String): Request {
return POST(
url,
body = FormBody.Builder()
.add("adult", "true")
.build()
)
}
override fun chapterListRequest(manga: SManga) = allowAdult(super.chapterListRequest(manga))
override fun chapterListSelector() = "div.group div.element, div.list div.element"
open val chapterDateSelector = "div.meta_r"
open val chapterUrlSelector = "a[title]"
override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select(chapterUrlSelector).first()
val dateElement = element.select(chapterDateSelector).first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = dateElement.text()?.let { parseChapterDate(it.substringAfter(", ")) }
?: 0
return chapter
}
open fun parseChapterDate(date: String): Long? {
val lcDate = date.toLowerCase()
if (lcDate.endsWith(" ago"))
parseRelativeDate(lcDate)?.let { return it }
// Handle 'yesterday' and 'today', using midnight
var relativeDate: Calendar? = null
// Result parsed but no year, copy current year over
when {
lcDate.startsWith("yesterday") -> {
relativeDate = Calendar.getInstance()
relativeDate.add(Calendar.DAY_OF_MONTH, -1) // yesterday
relativeDate.set(Calendar.HOUR_OF_DAY, 0)
relativeDate.set(Calendar.MINUTE, 0)
relativeDate.set(Calendar.SECOND, 0)
relativeDate.set(Calendar.MILLISECOND, 0)
}
lcDate.startsWith("today") -> {
relativeDate = Calendar.getInstance()
relativeDate.set(Calendar.HOUR_OF_DAY, 0)
relativeDate.set(Calendar.MINUTE, 0)
relativeDate.set(Calendar.SECOND, 0)
relativeDate.set(Calendar.MILLISECOND, 0)
}
lcDate.startsWith("tomorrow") -> {
relativeDate = Calendar.getInstance()
relativeDate.add(Calendar.DAY_OF_MONTH, +1) // tomorrow
relativeDate.set(Calendar.HOUR_OF_DAY, 0)
relativeDate.set(Calendar.MINUTE, 0)
relativeDate.set(Calendar.SECOND, 0)
relativeDate.set(Calendar.MILLISECOND, 0)
}
}
relativeDate?.timeInMillis?.let {
return it
}
var result = DATE_FORMAT_1.parseOrNull(date)
for (dateFormat in DATE_FORMATS_WITH_ORDINAL_SUFFIXES) {
if (result == null)
result = dateFormat.parseOrNull(date)
else
break
}
for (dateFormat in DATE_FORMATS_WITH_ORDINAL_SUFFIXES_NO_YEAR) {
if (result == null) {
result = dateFormat.parseOrNull(date)
if (result != null) {
// Result parsed but no year, copy current year over
result = Calendar.getInstance().apply {
time = result!!
set(Calendar.YEAR, Calendar.getInstance().get(Calendar.YEAR))
}.time
}
} else break
}
return result?.time ?: 0L
}
/**
* Parses dates in this form:
* `11 days ago`
*/
private fun parseRelativeDate(date: String): Long? {
val trimmedDate = date.split(" ")
if (trimmedDate[2] != "ago") return null
val number = trimmedDate[0].toIntOrNull() ?: return null
val unit = trimmedDate[1].removeSuffix("s") // Remove 's' suffix
val now = Calendar.getInstance()
// Map English unit to Java unit
val javaUnit = when (unit) {
"year", "yr" -> Calendar.YEAR
"month" -> Calendar.MONTH
"week", "wk" -> Calendar.WEEK_OF_MONTH
"day" -> Calendar.DAY_OF_MONTH
"hour", "hr" -> Calendar.HOUR
"minute", "min" -> Calendar.MINUTE
"second", "sec" -> Calendar.SECOND
else -> return null
}
now.add(javaUnit, -number)
return now.timeInMillis
}
private fun SimpleDateFormat.parseOrNull(string: String): Date? {
return try {
parse(string)
} catch (e: ParseException) {
null
}
}
override fun pageListRequest(chapter: SChapter) = allowAdult(super.pageListRequest(chapter))
override fun pageListParse(document: Document): List<Page> {
val doc = document.toString()
val jsonstr = doc.substringAfter("var pages = ").substringBefore(";")
val json = JsonParser().parse(jsonstr).asJsonArray
val pages = mutableListOf<Page>()
json.forEach {
// Create dummy element to resolve relative URL
val absUrl = document.createElement("a")
.attr("href", it["url"].asString)
.absUrl("href")
pages.add(Page(pages.size, "", absUrl))
}
return pages
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
companion object {
private val ORDINAL_SUFFIXES = listOf("st", "nd", "rd", "th")
private val DATE_FORMAT_1 = SimpleDateFormat("yyyy.MM.dd", Locale.US)
private val DATE_FORMATS_WITH_ORDINAL_SUFFIXES = ORDINAL_SUFFIXES.map {
SimpleDateFormat("dd'$it' MMMM, yyyy", Locale.US)
}
private val DATE_FORMATS_WITH_ORDINAL_SUFFIXES_NO_YEAR = ORDINAL_SUFFIXES.map {
SimpleDateFormat("dd'$it' MMMM", Locale.US)
}
}
}

View File

@ -1,270 +0,0 @@
package eu.kanade.tachiyomi.extension.all.foolslide
import android.app.Application
import android.content.SharedPreferences
import android.support.v7.preference.EditTextPreference
import android.support.v7.preference.PreferenceScreen
import android.widget.Toast
import com.github.salomonbrys.kotson.get
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class FoolSlideFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
SenseScans(),
KireiCake(),
SilentSky(),
Mangatellers(),
IskultripScans(),
AnataNoMotokare(),
DeathTollScans(),
YuriIsm(),
AjiaNoScantrad(),
OneTimeScans(),
MangaScouts(),
StormInHeaven(),
Lilyreader(),
Russification(),
EvilFlowers(),
LupiTeam(),
HentaiCafe(),
TheCatScans(),
ZandynoFansub(),
HelveticaScans(),
KirishimaFansub(),
PowerMangaIT(),
BaixarHentai(),
HNIScantrad(),
HNIScantradEN(),
PhoenixScans(),
GTO(),
FallenWorldOrder(),
NIFTeam(),
TuttoAnimeManga(),
Customizable(),
TortugaCeviri(),
Rama(),
Mabushimajo(),
MenudoFansub()
)
}
class MenudoFansub : FoolSlide("Menudo-Fansub", "http://www.menudo-fansub.com", "es", "/slide")
class TheCatScans : FoolSlide("The Cat Scans", "https://reader2.thecatscans.com/", "en")
class SenseScans : FoolSlide("Sense-Scans", "http://sensescans.com", "en", "/reader")
class KireiCake : FoolSlide("Kirei Cake", "https://reader.kireicake.com", "en") {
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
description = document.select("$mangaDetailsInfoSelector li:has(b:contains(description))")
.first()?.ownText()?.substringAfter(":")
thumbnail_url = getDetailsThumbnail(document)
}
}
}
class SilentSky : FoolSlide("Silent Sky", "https://reader.silentsky-scans.net", "en")
class Mangatellers : FoolSlide("Mangatellers", "http://www.mangatellers.gr", "en", "/reader/reader") {
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl$urlModifier/list/$page/", headers)
}
}
class IskultripScans : FoolSlide("Iskultrip Scans", "https://maryfaye.net", "en", "/reader")
class AnataNoMotokare : FoolSlide("Anata no Motokare", "https://motokare.xyz", "en", "/reader")
class DeathTollScans : FoolSlide("Death Toll Scans", "https://reader.deathtollscans.net", "en")
class YuriIsm : FoolSlide("Yuri-ism", "https://www.yuri-ism.net", "en", "/slide")
class AjiaNoScantrad : FoolSlide("Ajia no Scantrad", "https://www.ajianoscantrad.fr", "fr", "/reader")
class OneTimeScans : FoolSlide("One Time Scans", "https://reader.otscans.com", "en")
class MangaScouts : FoolSlide("MangaScouts", "http://onlinereader.mangascouts.org", "de")
class StormInHeaven : FoolSlide("Storm in Heaven", "https://www.storm-in-heaven.net", "it", "/reader-sih")
class Lilyreader : FoolSlide("Lilyreader", "https://manga.smuglo.li", "en")
class Russification : FoolSlide("Русификация", "https://rusmanga.ru", "ru")
class EvilFlowers : FoolSlide("Evil Flowers", "https://reader.evilflowers.com", "en")
class LupiTeam : FoolSlide("LupiTeam", "https://lupiteam.net", "it", "/reader") {
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select(mangaDetailsInfoSelector).first().text()
val manga = SManga.create()
manga.author = infoElement.substringAfter("Autore: ").substringBefore("Artista: ")
manga.artist = infoElement.substringAfter("Artista: ").substringBefore("Target: ")
val stato = infoElement.substringAfter("Stato: ").substringBefore("Trama: ").substring(0, 8)
manga.status = when (stato) {
"In corso" -> SManga.ONGOING
"Completa" -> SManga.COMPLETED
"Licenzia" -> SManga.LICENSED
else -> SManga.UNKNOWN
}
manga.description = infoElement.substringAfter("Trama: ")
manga.thumbnail_url = getDetailsThumbnail(document)
return manga
}
}
class ZandynoFansub : FoolSlide("Zandy no Fansub", "https://zandynofansub.aishiteru.org", "en", "/reader")
class HelveticaScans : FoolSlide("Helvetica Scans", "https://helveticascans.com", "en", "/r")
class KirishimaFansub : FoolSlide("Kirishima Fansub", "https://www.kirishimafansub.net", "es", "/lector")
class PowerMangaIT : FoolSlide("PowerManga", "https://reader.powermanga.org", "it", "")
@Nsfw
class BaixarHentai : FoolSlide("Baixar Hentai", "https://leitura.baixarhentai.net", "pt-BR") {
// Hardcode the id because the language wasn't specific.
override val id: Long = 8908032188831949972
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
title = document.select("h1.title").text()
thumbnail_url = getDetailsThumbnail(document, "div.title a")
}
}
}
class HNIScantrad : FoolSlide("HNI-Scantrad", "https://hni-scantrad.com", "fr", "/lel")
class HNIScantradEN : FoolSlide("HNI-Scantrad", "https://hni-scantrad.com", "en", "/eng/lel") {
override val supportsLatest = false
override fun popularMangaRequest(page: Int) = GET(baseUrl + urlModifier, headers)
override fun popularMangaSelector() = "div.listed"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
element.select("a:has(h3)").let {
title = it.text()
setUrlWithoutDomain(it.attr("abs:href"))
}
thumbnail_url = element.select("img").attr("abs:src")
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl$urlModifier/?manga=${query.replace(" ", "+")}")
override fun searchMangaSelector(): String = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun chapterListSelector() = "div.theList > a"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
name = element.select("div.chapter b").text()
setUrlWithoutDomain(element.attr("abs:href"))
}
}
override fun pageListParse(response: Response): List<Page> {
return Regex("""imageArray\[\d+]='(.*)'""").findAll(response.body()!!.string()).toList().mapIndexed { i, mr ->
Page(i, "", "$baseUrl$urlModifier/${mr.groupValues[1]}")
}
}
}
class PhoenixScans : FoolSlide("The Phoenix Scans", "https://www.phoenixscans.com", "it", "/reader")
class GTO : FoolSlide("GTO The Great Site", "https://www.gtothegreatsite.net", "it", "/reader")
class FallenWorldOrder : FoolSlide("Fall World Reader", "https://faworeader.altervista.org", "it", "/slide")
class NIFTeam : FoolSlide("NIFTeam", "http://read-nifteam.info", "it", "/slide")
class TuttoAnimeManga : FoolSlide("TuttoAnimeManga", "https://tuttoanimemanga.net", "it", "/slide")
class Customizable : ConfigurableSource, FoolSlide("Customizable", "", "other") {
override val baseUrl: String by lazy { getPrefBaseUrl() }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
key = BASE_URL_PREF_TITLE
title = BASE_URL_PREF_TITLE
summary = BASE_URL_PREF_SUMMARY
this.setDefaultValue(DEFAULT_BASEURL)
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "Default: $DEFAULT_BASEURL"
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(BASE_URL_PREF, newValue as String).commit()
Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(baseUrlPref)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val baseUrlPref = EditTextPreference(screen.context).apply {
key = BASE_URL_PREF_TITLE
title = BASE_URL_PREF_TITLE
summary = BASE_URL_PREF_SUMMARY
this.setDefaultValue(DEFAULT_BASEURL)
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "Default: $DEFAULT_BASEURL"
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(BASE_URL_PREF, newValue as String).commit()
Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(baseUrlPref)
}
/**
* Tell the user to include /directory/ in the URL even though we remove it
* To increase the chance they input a usable URL
*/
private fun getPrefBaseUrl() = preferences.getString(BASE_URL_PREF, DEFAULT_BASEURL)!!.substringBefore("/directory")
companion object {
private const val DEFAULT_BASEURL = "https://127.0.0.1"
private const val BASE_URL_PREF_TITLE = "Example URL: https://domain.com/path_to/directory/"
private const val BASE_URL_PREF = "overrideBaseUrl_v${BuildConfig.VERSION_NAME}"
private const val BASE_URL_PREF_SUMMARY = "Connect to a designated FoolSlide server"
private const val RESTART_TACHIYOMI = "Restart Tachiyomi to apply new setting."
}
}
class TortugaCeviri : FoolSlide("Tortuga Ceviri", "http://tortuga-ceviri.com", "tr", "/okuma")
class Rama : FoolSlide("Rama", "https://www.ramareader.it", "it", "/read")
class Mabushimajo : FoolSlide("Mabushimajo", "http://mabushimajo.com", "tr", "/onlineokuma")

View File

@ -1,351 +0,0 @@
package eu.kanade.tachiyomi.extension.all.foolslide
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
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
@Nsfw
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()
if (urlElement != null) {
setUrlWithoutDomain(urlElement.attr("href"))
thumbnail_url = urlElement.child(0).attr("src")
} else {
setUrlWithoutDomain(element.select(".entry-title a").attr("href"))
}
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:not(#post-0)"
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")
val genres = mutableListOf<String>()
document.select(".content a[rel=tag]").forEach { element ->
if (!element.attr("href").contains("artist"))
genres.add(element.text())
else {
artist = element.text()
author = element.text()
}
}
status = SManga.COMPLETED
genre = genres.joinToString(", ")
}
// 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 {
// Some URLs wrongly end with "<br />\n" and need to be removed
// Example: https://hentai.cafe/hc.fyi/12106
setUrlWithoutDomain(response.asJsoup().select("[title=Read]").attr("href").replace("<br />\\s*".toRegex(), ""))
name = "Chapter"
chapter_number = 1f
}
)
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<ArtistFilter>()?.let { f ->
if (f.state.isNotBlank()) {
requireNoUrl()
url = "/hc.fyi/artist/${f.state
.trim()
.toLowerCase()
.replace(ARTIST_INVALID_CHAR_REGEX, "-")}/"
}
}
filters.findInstance<BookFilter>()?.let { f ->
if (f.state) {
requireNoUrl()
url = "/hc.fyi/category/book/"
}
}
filters.findInstance<TagFilter>()?.let { f ->
if (f.state != 0) {
requireNoUrl()
url = "/hc.fyi/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<MangasPage> {
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<ArtistFilter>()?.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>(
"Tag",
arrayOf(
Tag("", "<select>"),
Tag("ahegao", "Ahegao"),
Tag("anal", "Anal"),
Tag("apron", "Apron"),
Tag("ashioki", "Ashioki"),
Tag("bakunyuu", "Bakunyuu"),
Tag("bathroom-sex", "Bathroom sex"),
Tag("beauty-mark", "Beauty mark"),
Tag("big-ass", "Big ass"),
Tag("big-breast", "Big breast"),
Tag("big-dick", "Big dick"),
Tag("biting", "Biting"),
Tag("black-mail", "Blackmail"),
Tag("blindfold", "Blindfold"),
Tag("blowjob", "Blowjob"),
Tag("body-swap", "Body swap"),
Tag("bondage", "Bondage"),
Tag("booty", "Booty"),
Tag("bride", "Bride"),
Tag("bukkake", "Bukkake"),
Tag("bunny-girl", "Bunny girl"),
Tag("busty", "Busty"),
Tag("cat", "Cat"),
Tag("cat-girl", "Cat girl"),
Tag("catgirl", "Catgirl"),
Tag("cheating", "Cheating"),
Tag("cheerleader", "Cheerleader"),
Tag("chikan", "Chikan"),
Tag("christmas", "Christmas"),
Tag("chubby", "Chubby"),
Tag("color", "Color"),
Tag("comedy", "Comedy"),
Tag("condom", "Condom"),
Tag("cosplay", "Cosplay"),
Tag("creampie", "Creampie"),
Tag("crossdressing", "Crossdressing"),
Tag("crotch-tattoo", "Crotch tattoo"),
Tag("cunnilingus", "Cunnilingus"),
Tag("dark-skin", "Dark skin"),
Tag("deepthroat", "Deepthroat"),
Tag("defloration", "Defloration"),
Tag("devil", "Devil"),
Tag("double-penetration", "Double penetration"),
Tag("doujin", "Doujin"),
Tag("doujinshi", "Doujinshi"),
Tag("drama", "Drama"),
Tag("drug", "Drug"),
Tag("drunk", "Drunk"),
Tag("elf", "Elf"),
Tag("exhibitionism", "Exhibitionism"),
Tag("eyebrows", "Eyebrows"),
Tag("eyepatch", "Eyepatch"),
Tag("facesitting", "Facesitting"),
Tag("fangs", "Fangs"),
Tag("fantasy", "Fantasy"),
Tag("fellatio", "Fellatio"),
Tag("femboy", "Femboy"),
Tag("femdom", "Femdom"),
Tag("filming", "Filming"),
Tag("flat-chest", "Flat chest"),
Tag("footjob", "Footjob"),
Tag("freckles", "Freckles"),
Tag("full-color", "Full color"),
Tag("furry", "Furry"),
Tag("futanari", "Futanari"),
Tag("gangbang", "Gangbang"),
Tag("gender-bender", "Gender bender"),
Tag("genderbend", "Genderbend"),
Tag("girls4m", "Girls4m"),
Tag("glasses", "Glasses"),
Tag("group", "Group"),
Tag("gyaru", "Gyaru"),
Tag("hairy", "Hairy"),
Tag("hairy-armpit", "Hairy armpit"),
Tag("handjob", "Handjob"),
Tag("harem", "Harem"),
Tag("headphones", "Headphones"),
Tag("heart-pupils", "Heart pupils"),
Tag("hentai", "Hentai"),
Tag("historical", "Historical"),
Tag("horns", "Horns"),
Tag("horror", "Horror"),
Tag("housewife", "Housewife"),
Tag("huge-boobs", "Huge-boobs"),
Tag("humiliation", "Humiliation"),
Tag("idol", "Idol"),
Tag("imouto", "Imouto"),
Tag("impregnation", "Impregnation"),
Tag("incest", "Incest"),
Tag("inseki", "Inseki"),
Tag("inverted-nipples", "Inverted nipples"),
Tag("irrumatio", "Irrumatio"),
Tag("isekai", "Isekai"),
Tag("kemono-mimi", "Kemono mimi"),
Tag("kimono", "Kimono"),
Tag("kogal", "Kogal"),
Tag("lactation", "Lactation"),
Tag("large-breast", "Large breast"),
Tag("lingerie", "Lingerie"),
Tag("loli", "Loli"),
Tag("love-hotel", "Love hotel"),
Tag("magical-girl", "Magical girl"),
Tag("maid", "Maid"),
Tag("masturbation", "Masturbation"),
Tag("miko", "Miko"),
Tag("milf", "Milf"),
Tag("mind-break", "Mind break"),
Tag("mind-control", "Mind control"),
Tag("monster-girl", "Monster girl"),
Tag("muscles", "Muscles"),
Tag("nakadashi", "Nakadashi"),
Tag("naked-apron", "Naked apron"),
Tag("netorare", "Netorare"),
Tag("netorase", "Netorase"),
Tag("netori", "Netori"),
Tag("ninja", "Ninja"),
Tag("nun", "Nun"),
Tag("nurse", "Nurse"),
Tag("office-lady", "Office lady"),
Tag("ojousama", "Ojousama"),
Tag("old-man", "Old man"),
Tag("onani", "Onani"),
Tag("oni", "Oni"),
Tag("orgasm-denial", "Orgasm denial"),
Tag("osananajimi", "Osananajimi"),
Tag("pailoli", "Pailoli"),
Tag("paizuri", "Paizuri"),
Tag("pegging", "Pegging"),
Tag("petite", "Petite"),
Tag("pettanko", "Pettanko"),
Tag("ponytail", "Ponytail"),
Tag("pregnant", "Pregnant"),
Tag("prositution", "Prositution"),
Tag("pubic-hair", "Pubic Hair"),
Tag("qipao", "Qipao"),
Tag("rape", "Rape"),
Tag("reverse-rape", "Reverse rape"),
Tag("rimjob", "Rimjob"),
Tag("schoolgirl", "Schoolgirl"),
Tag("schoolgirl-outfit", "Schoolgirl outfit"),
Tag("sci-fi", "Sci-fi"),
Tag("senpai", "Senpai"),
Tag("sex", "Sex"),
Tag("sex-toys", "Sex toys"),
Tag("shimapan", "Shimapan"),
Tag("shota", "Shota"),
Tag("shouta", "Shouta"),
Tag("sister", "Sister"),
Tag("sleeping", "Sleeping"),
Tag("small-breast", "Small breast"),
Tag("socks", "Socks"),
Tag("spats", "Spats"),
Tag("spread", "Spread"),
Tag("squirting", "Squirting"),
Tag("stocking", "Stocking"),
Tag("stockings", "Stockings"),
Tag("succubus", "Succubus"),
Tag("swimsuit", "Swimsuit"),
Tag("swinging", "Swinging"),
Tag("tall-girl", "Tall-girl"),
Tag("tanlines", "Tanlines"),
Tag("teacher", "Teacher"),
Tag("tentacles", "Tentacles"),
Tag("threesome", "Threesome"),
Tag("time-stop", "Time stop"),
Tag("tomboy", "Tomboy"),
Tag("toys", "Toys"),
Tag("trans", "Trans"),
Tag("tsundere", "Tsundere"),
Tag("twin", "Twin"),
Tag("twintails", "Twintails"),
Tag("ugly-bastard", "Ugly bastard"),
Tag("uncensored", "Uncensored"),
Tag("unlimited", "Unlimited"),
Tag("urination", "Urination"),
Tag("vanilla", "Vanilla"),
Tag("virgin", "Virgin"),
Tag("vomit", "Vomit"),
Tag("voyeurism", "Voyeurism"),
Tag("waitress", "Waitress"),
Tag("x-ray", "X-Ray"),
Tag("yandere", "Yandere"),
Tag("yukata", "Yukata"),
Tag("yuri", "Yuri")
)
)
class Tag(val name: String, private 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 <reified T> Iterable<*>.findInstance() = find { it is T } as? T