diff --git a/lib-multisrc/hiper/assets/i18n/messages_en.properties b/lib-multisrc/hiper/assets/i18n/messages_en.properties new file mode 100644 index 00000000000..b610d4e137a --- /dev/null +++ b/lib-multisrc/hiper/assets/i18n/messages_en.properties @@ -0,0 +1,18 @@ +genre_filter_title=Genres +type_filter_title=Type +rating_filter_title=Rating +status_filter_title=Status +status_all=All +status_ongoing=Ongoing +status_onhiatus=Hiatus +status_completed=Completed +status_canceled=Canceled +sort_by_filter_title=Sort By +sort_by_relevance=Relevance +sort_by_popular=Popularity +sort_by_score=Score +sort_by_recent=Recent Updated +sort_by_newest=Newest +sort_by_oldest=Oldest +pref_rating_title=Default max rating +pref_rating_summary=Restricts content to the selected rating or below.\nCurrently: diff --git a/lib-multisrc/hiper/assets/i18n/messages_pt_br.properties b/lib-multisrc/hiper/assets/i18n/messages_pt_br.properties new file mode 100644 index 00000000000..d9e7aa72ed7 --- /dev/null +++ b/lib-multisrc/hiper/assets/i18n/messages_pt_br.properties @@ -0,0 +1,18 @@ +genre_filter_title=Gêneros +type_filter_title=Tipo +rating_filter_title=Classificação +status_filter_title=Estado +status_all=Todos +status_ongoing=Em andamento +status_onhiatus=Hiato +status_completed=Completo +status_canceled=Cancelado +sort_by_filter_title=Ordenar por +sort_by_relevance=Relevância +sort_by_popular=Popularidade +sort_by_score=Nota +sort_by_recent=Atualizados recentemente +sort_by_newest=Mais recentes +sort_by_oldest=Mais antigos +pref_rating_title=Classificação máxima padrão +pref_rating_summary=Restringe o conteúdo à classificação selecionada ou inferior.\nAtualmente: diff --git a/lib-multisrc/hiper/build.gradle.kts b/lib-multisrc/hiper/build.gradle.kts index 8b9be3784b3..bfe32342e3f 100644 --- a/lib-multisrc/hiper/build.gradle.kts +++ b/lib-multisrc/hiper/build.gradle.kts @@ -2,7 +2,11 @@ plugins { alias(kei.plugins.multisrc) } +dependencies { + api(project(":lib:i18n")) +} + keiyoushi { - baseVersionCode = 4 - libVersion = "1.4" + baseVersionCode = 5 + libVersion = "1.6" } diff --git a/lib-multisrc/hiper/src/eu/kanade/tachiyomi/multisrc/hiper/Dto.kt b/lib-multisrc/hiper/src/eu/kanade/tachiyomi/multisrc/hiper/Dto.kt index ea65ff5926f..6964c780fc2 100644 --- a/lib-multisrc/hiper/src/eu/kanade/tachiyomi/multisrc/hiper/Dto.kt +++ b/lib-multisrc/hiper/src/eu/kanade/tachiyomi/multisrc/hiper/Dto.kt @@ -5,6 +5,8 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import keiyoushi.utils.tryParse import kotlinx.serialization.Serializable +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import java.text.SimpleDateFormat import java.util.Locale @@ -24,6 +26,8 @@ class MangaDto( val genres: List? = null, val authors: List? = null, val artists: List? = null, + val type: String?, + val contentRating: String?, ) { fun toSManga(mangaPath: String) = SManga.create().apply { title = this@MangaDto.title @@ -31,7 +35,7 @@ class MangaDto( thumbnail_url = coverUrl artist = artists?.joinToString() author = authors?.joinToString() - genre = genres?.joinToString() + genre = ((genres ?: emptyList()) + listOfNotNull(type, contentRating)).joinToString() status = when (this@MangaDto.status?.lowercase()) { "ongoing" -> SManga.ONGOING "hiatus" -> SManga.ON_HIATUS @@ -39,7 +43,10 @@ class MangaDto( "completed" -> SManga.COMPLETED else -> SManga.UNKNOWN } - url = "/$mangaPath/$slug#${this@MangaDto.id}" + url = "/$mangaPath/$slug" + memo = buildJsonObject { + put("mangaId", id) + } initialized = true } } diff --git a/lib-multisrc/hiper/src/eu/kanade/tachiyomi/multisrc/hiper/Hiper.kt b/lib-multisrc/hiper/src/eu/kanade/tachiyomi/multisrc/hiper/Hiper.kt index f4e264ca1dd..6d0038a4ed5 100644 --- a/lib-multisrc/hiper/src/eu/kanade/tachiyomi/multisrc/hiper/Hiper.kt +++ b/lib-multisrc/hiper/src/eu/kanade/tachiyomi/multisrc/hiper/Hiper.kt @@ -1,23 +1,27 @@ package eu.kanade.tachiyomi.multisrc.hiper -import android.os.Handler -import android.os.Looper -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse -import android.webkit.WebView -import android.webkit.WebViewClient +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.ConfigurableSource 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.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.model.SMangaUpdate import eu.kanade.tachiyomi.util.asJsoup -import keiyoushi.utils.applicationContext +import keiyoushi.lib.i18n.Intl +import keiyoushi.network.get +import keiyoushi.source.KeiSource import keiyoushi.utils.get +import keiyoushi.utils.getLongOrNull +import keiyoushi.utils.getPreferencesLazy import keiyoushi.utils.parseAs +import keiyoushi.utils.runWebView +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonArray @@ -27,72 +31,77 @@ import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject import okhttp3.Headers +import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request +import okhttp3.OkHttpClient import okhttp3.Response -import rx.Observable import java.io.IOException -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.seconds -abstract class Hiper : HttpSource() { +abstract class Hiper : + KeiSource(), + ConfigurableSource { - protected open val mangaPath: String = "manga" - - override val supportsLatest: Boolean = true - - override fun headersBuilder(): Headers.Builder = super.headersBuilder() - .set("Referer", "$baseUrl/") + protected val preferences by getPreferencesLazy() - private val acceptHeaders = headersBuilder() - .set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - .build() - - private val handler by lazy { Handler(Looper.getMainLooper()) } + protected open val mangaPath: String = "manga" private var wvHeaders: Headers? = null - override val client = network.client.newBuilder() - .addInterceptor { chain -> - var request = chain.request() + private val wvMutex = Mutex() - wvHeaders?.let { - request = request.newBuilder() - .headers(it) - .build() - } + protected fun OkHttpClient.Builder.addHiperAuthInterceptor(): OkHttpClient.Builder = addInterceptor { chain -> + var request = chain.request() - val response = chain.proceed(request) + wvHeaders?.let { + request = request.newBuilder() + .headers(it) + .build() + } - // Fetch baseUrl with accept headers which then populates a cookie - if (response.code == 401) { - response.close() - network.client.newCall(GET(baseUrl, acceptHeaders)).execute().close() - chain.proceed(chain.request()) - } else { - response - } + val response = chain.proceed(request) + + // Fetch baseUrl with accept headers which then populates a cookie + if (response.code == 401) { + response.close() + val acceptHeaders = headers.newBuilder() + .set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .build() + network.client.newCall(GET(baseUrl, acceptHeaders)).execute().close() + chain.proceed(chain.request()) + } else { + response } - .build() + } - // ============================ Popular ==================================== + override fun OkHttpClient.Builder.configureClient(): OkHttpClient.Builder = addHiperAuthInterceptor() - private val popularFilter = FilterList(OrderByFilter("", arrayOf("" to "popular"))) + protected val intl = Intl( + language = lang, + baseLanguage = "en", + availableLanguages = setOf("en", "pt-BR"), + classLoader = this::class.java.classLoader!!, + ) - override fun popularMangaRequest(page: Int): Request = searchMangaRequest(page, "", popularFilter) + // ============================ Popular ==================================== - override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response) + protected open val popularFilter = FilterList(OrderByFilter("", arrayOf("" to "popular"))) - // ============================ Latest ==================================== - private val latestFilter = FilterList(OrderByFilter("", arrayOf("" to "newest"))) + override suspend fun getPopularManga(page: Int): MangasPage = getSearchMangaList(page, "", popularFilter) - override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(page, "", latestFilter) + // ============================ Latest ==================================== + protected open val latestFilter = FilterList(OrderByFilter("", arrayOf("" to "recent"))) - override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response) + override suspend fun getLatestUpdates(page: Int): MangasPage = getSearchMangaList(page, "", latestFilter) // ============================ Search ==================================== - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage { val limit = 30 + val typeValue = filters.filterIsInstance().firstOrNull()?.selected() + val statusValue = filters.filterIsInstance().firstOrNull()?.selected() + val ratingValue = filters.filterIsInstance().firstOrNull()?.selected() + val genresValue = filters.filterIsInstance().firstOrNull()?.checked.orEmpty() + val input = buildJsonObject { putJsonObject("0") { putJsonObject("json") { @@ -101,10 +110,14 @@ abstract class Hiper : HttpSource() { put("sort", filter.selected()) } putJsonObject("filters") { - put("genres", null) - put("type", null) - put("status", null) - put("contentRating", null) + if (genresValue.isEmpty()) { + put("genres", null) + } else { + putJsonArray("genres") { genresValue.forEach { add(it) } } + } + put("type", typeValue) + put("status", statusValue) + put("contentRating", ratingValue) put("author", null) put("artist", null) put("year", null) @@ -112,14 +125,14 @@ abstract class Hiper : HttpSource() { put("limit", limit) put("offset", (page - 1) * limit) - put("maxRating", "pornographic") + put("maxRating", preferences.getString(MAX_RATING_PREF, MAX_RATING_DEFAULT)) } putJsonObject("meta") { putJsonObject("values") { - put("filters.genres", buildJsonArray { add("undefined") }) - put("filters.type", buildJsonArray { add("undefined") }) - put("filters.status", buildJsonArray { add("undefined") }) - put("filters.contentRating", buildJsonArray { add("undefined") }) + if (genresValue.isEmpty()) put("filters.genres", buildJsonArray { add("undefined") }) + if (typeValue == null) put("filters.type", buildJsonArray { add("undefined") }) + if (statusValue == null) put("filters.status", buildJsonArray { add("undefined") }) + if (ratingValue == null) put("filters.contentRating", buildJsonArray { add("undefined") }) put("filters.author", buildJsonArray { add("undefined") }) put("filters.artist", buildJsonArray { add("undefined") }) put("filters.year", buildJsonArray { add("undefined") }) @@ -133,10 +146,10 @@ abstract class Hiper : HttpSource() { .addQueryParameter("input", input) .build() - return GET(url, headers) + return parseSearchMangaList(client.get(url)) } - override fun searchMangaParse(response: Response): MangasPage { + protected open fun parseSearchMangaList(response: Response): MangasPage { val element = response.parseAs>().first() val dto = element["result"]["data"]["json"]?.parseAs() ?: return MangasPage(emptyList(), false) return MangasPage(dto.hits.map { it.toSManga(mangaPath) }, dto.hits.isNotEmpty()) @@ -146,8 +159,18 @@ abstract class Hiper : HttpSource() { override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}" - override fun mangaDetailsRequest(manga: SManga): Request { + protected open suspend fun getMangaDetails(manga: SManga): SManga? { val slug = manga.url.getSlug() + return getMangaBySlug(slug) + } + + override suspend fun getMangaByUrl(url: HttpUrl): SManga? { + val slug = url.toString().getSlug() + return getMangaBySlug(slug) + } + + protected open suspend fun getMangaBySlug(slug: String): SManga? { + if (slug.isBlank()) return null val input = buildJsonObject { putJsonObject("0") { @@ -169,10 +192,11 @@ abstract class Hiper : HttpSource() { .addQueryParameter("batch", "1") .addQueryParameter("input", input.toString()) .build() - return GET(url, headers) + + return parseMangaDetails(client.get(url)) } - override fun mangaDetailsParse(response: Response): SManga { + protected open fun parseMangaDetails(response: Response): SManga { val element = response.parseAs>().last() return element["result"]["data"]["json"]!!.parseAs().toSManga(mangaPath) } @@ -184,9 +208,11 @@ abstract class Hiper : HttpSource() { return "$baseUrl/$mangaPath/$slug/${chapter.getNumber()}".removeSuffix(".0") } - override fun chapterListRequest(manga: SManga): Request { - val mangaId = manga.url.substringAfterLast("#").toLongOrNull() - ?: throw IOException("Migrate from $name to $name") + protected open suspend fun getChapterList(manga: SManga): List { + val mangaId = manga.memo.getLongOrNull("mangaId") + ?: manga.url.substringAfterLast("#").toLongOrNull() // Fallback for old manga URLs + ?: throw IOException("Refresh chapter list") + val input = buildJsonObject { putJsonObject("0") { putJsonObject("json") { @@ -218,24 +244,41 @@ abstract class Hiper : HttpSource() { } } - val url = "$baseUrl/api/trpc/auth.me,comments.list,series.chapters".toHttpUrl().newBuilder() + val url = "$baseUrl/api/trpc/auth.me,series.chapters".toHttpUrl().newBuilder() .addQueryParameter("batch", "1") .addQueryParameter("input", input.toString()) - .fragment(manga.url) .build() - return GET(url, headers) - } - override fun chapterListParse(response: Response): List { - val mangaPath = response.request.url.fragment!! + val response = client.get(url) val element = response.parseAs>().last() val chaptersDTO = element["result"]["data"]["json"]!!.parseAs>() - return chaptersDTO.map { it.toSChapter(mangaPath) } + return chaptersDTO.map { it.toSChapter(manga.url) } } + // ============================ Manga updates ============================= + + override suspend fun fetchMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate { + val updatedManga = if (fetchDetails) getMangaDetails(manga) else null + val updatedChapters = if (fetchChapters) getChapterList(manga) else null + + return SMangaUpdate(updatedManga ?: manga, updatedChapters ?: chapters) + } + + // ============================ Related manga ============================== + + override val supportsRelatedMangas = false + override val supportRelatedMangasBySearch = true + + override suspend fun fetchRelatedMangaList(manga: SManga): List = throw UnsupportedOperationException() + // ============================ Pages ===================================== - override fun pageListRequest(chapter: SChapter): Request { + protected open fun pageListUrl(chapter: SChapter): HttpUrl { val slug = chapter.url.getSlug() val input = buildJsonObject { @@ -265,32 +308,28 @@ abstract class Hiper : HttpSource() { } } - val url = "$baseUrl/api/trpc/auth.me,series.bySlug,reader.chapterPages".toHttpUrl().newBuilder() + return "$baseUrl/api/trpc/auth.me,series.bySlug,reader.chapterPages".toHttpUrl().newBuilder() .addQueryParameter("batch", "1") .addQueryParameter("input", input.toString()) .build() - - return GET(url, headers) } - override fun fetchPageList(chapter: SChapter): Observable> = Observable.fromCallable { + override suspend fun getPageList(chapter: SChapter): List { val chapterUrl = getChapterUrl(chapter) + val url = pageListUrl(chapter) - val request = pageListRequest(chapter) - val response = client.newCall(request).execute() - val resJson = parsePages(response) + val pages = parsePages(client.get(url, ensureSuccess = false)) - resJson?.takeIf { it.isNotEmpty() } ?: run { + return pages?.takeIf { it.isNotEmpty() } ?: run { fetchHeadersWv(chapterUrl) wvHeaders?.let { - val response = client.newCall(request).execute() - parsePages(response) ?: emptyList() + parsePages(client.get(url, ensureSuccess = false)) ?: emptyList() } ?: error("Failed to get headers") } } - private fun parsePages(response: Response): List? { + protected open fun parsePages(response: Response): List? { val elements = response.parseAs>() for (element in elements) { @@ -304,59 +343,136 @@ abstract class Hiper : HttpSource() { return pages.map(PageDto::toPage) } - @Synchronized - fun fetchHeadersWv(url: String) { - val latch = CountDownLatch(1) - var webView: WebView? = null - val doc = client.newCall(GET(url, headers)).execute().asJsoup() - - handler.post { - val wv = WebView(applicationContext).also { webView = it } - - wv.settings.javaScriptEnabled = true - wv.settings.domStorageEnabled = true - wv.settings.loadsImagesAutomatically = false - wv.settings.blockNetworkImage = true - wv.settings.userAgentString = headers["user-agent"] - - wv.webViewClient = object : WebViewClient() { - override fun shouldInterceptRequest( - view: WebView, - request: WebResourceRequest, - ): WebResourceResponse? { - val url = request.url.toString() - if (url.startsWith("$baseUrl/api")) { + protected open suspend fun fetchHeadersWv(url: String) { + wvMutex.withLock { + val doc = client.get(url).asJsoup() + + runWebView(timeout = 15.seconds) { + userAgent = headers["User-Agent"] ?: "" + blockImages = true + + interceptRequest { request -> + val requestUrl = request.url.toString() + if (requestUrl.startsWith("$baseUrl/api")) { val allHeaders = request.requestHeaders if (allHeaders.isNotEmpty()) { wvHeaders = headers.newBuilder().apply { allHeaders.forEach { (key, value) -> - headers.get(key) ?: add(key, value) + headers[key] ?: add(key, value) } }.build() } - latch.countDown() - return null + resolve(Unit) } - return super.shouldInterceptRequest(view, request) + null } + + loadData(doc.location(), doc.outerHtml()) } - wv.loadDataWithBaseURL(doc.location(), doc.outerHtml(), "text/html", "utf-8", null) } - - latch.await(15, TimeUnit.SECONDS) - handler.post { webView?.destroy() } } - override fun pageListParse(response: Response): List = throw UnsupportedOperationException() - - override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + // ============================ Preferences ================================ + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + key = MAX_RATING_PREF + title = intl["pref_rating_title"] + summary = "${intl["pref_rating_summary"]} %s" + entries = MAX_RATING_ENTRIES + entryValues = MAX_RATING_VALUES + setDefaultValue(MAX_RATING_DEFAULT) + }.let(screen::addPreference) + } // ============================ Filters ==================================== + override fun getFilterList(data: JsonElement?): FilterList = FilterList( + SortFilter(intl), + RatingFilter(intl), + TypeFilter(intl), + StatusFilter(intl), + GenresFilter(genresList, intl), + ) + open class OrderByFilter(displayName: String, private val vals: Array>, state: Int = 0) : Filter.Select(displayName, vals.map { it.first }.toTypedArray(), state) { fun selected() = vals[state].second } + class SortFilter(intl: Intl) : + OrderByFilter( + intl["sort_by_filter_title"], + arrayOf( + intl["sort_by_relevance"] to "relevance", + intl["sort_by_popular"] to "popular", + intl["sort_by_score"] to "score", + intl["sort_by_recent"] to "recent", + intl["sort_by_newest"] to "newest", + intl["sort_by_oldest"] to "oldest", + "A-Z" to "alphabetical", + ), + ) + + open class SelectFilter(displayName: String, private val vals: Array>, state: Int = 0) : Filter.Select(displayName, vals.map { it.first }.toTypedArray(), state) { + fun selected(): String? = vals[state].second + } + + class RatingFilter(intl: Intl) : + SelectFilter( + intl["rating_filter_title"], + ( + listOf( + intl["status_all"] to null, + ) + MAX_RATING_ENTRIES.zip(MAX_RATING_VALUES).map { it.first to it.second } + ) + .toTypedArray(), + ) + + class TypeFilter(intl: Intl) : + SelectFilter( + intl["type_filter_title"], + arrayOf( + intl["status_all"] to null, + "Manga" to "manga", + "Manhwa" to "manhwa", + "Manhua" to "manhua", + "Novel" to "novel", + "Webtoon" to "webtoon", + "One Shot" to "one_shot", + ), + ) + + class StatusFilter(intl: Intl) : + SelectFilter( + intl["status_filter_title"], + arrayOf( + intl["status_all"] to null, + intl["status_ongoing"] to "ongoing", + intl["status_completed"] to "completed", + intl["status_onhiatus"] to "hiatus", + intl["status_canceled"] to "cancelled", + ), + ) + + class GenreCheckBox(val value: String) : Filter.CheckBox(value) + + class GenresFilter(entries: List, intl: Intl) : + Filter.Group( + intl["genre_filter_title"], + entries.map { GenreCheckBox(it) }, + ) { + val checked get() = state.filter { it.state }.map { it.value } + } + + protected open val genresList: List = emptyList() + + companion object { + private const val MAX_RATING_PREF = "MAX_RATING" + private const val MAX_RATING_DEFAULT = "pornographic" + private val MAX_RATING_ENTRIES = arrayOf("Pornographic", "Erotica", "Suggestive", "Safe") + private val MAX_RATING_VALUES = arrayOf("pornographic", "erotica", "suggestive", "safe") + } + // Old (New) // manga: /manga/ // chapter: /manga/slug(#ID)/ @@ -365,7 +481,7 @@ abstract class Hiper : HttpSource() { .substringBefore("/") .substringBefore("#") .takeIf(String::isNotBlank) - ?: throw IOException("Migrate from $name to $name") + ?: throw IOException("Refresh chapter list") fun SChapter.getNumber() = if (chapter_number > 0) { chapter_number diff --git a/src/en/hiperdex/build.gradle.kts b/src/en/hiperdex/build.gradle.kts index daa57b4f90a..a56af511620 100644 --- a/src/en/hiperdex/build.gradle.kts +++ b/src/en/hiperdex/build.gradle.kts @@ -8,13 +8,13 @@ keiyoushi { name = "Hiperdex" versionCode = 80 contentWarning = ContentWarning.MIXED - libVersion = "1.4" + libVersion = "1.6" theme = "hiper" source { lang = "en" baseUrl { - custom("https://hiperdex.com") + custom("https://hiperdex.tv") } } } diff --git a/src/en/hiperdex/src/eu/kanade/tachiyomi/extension/en/hiperdex/Hiperdex.kt b/src/en/hiperdex/src/eu/kanade/tachiyomi/extension/en/hiperdex/Hiperdex.kt index a7800008611..ea300165062 100644 --- a/src/en/hiperdex/src/eu/kanade/tachiyomi/extension/en/hiperdex/Hiperdex.kt +++ b/src/en/hiperdex/src/eu/kanade/tachiyomi/extension/en/hiperdex/Hiperdex.kt @@ -8,29 +8,26 @@ import androidx.preference.CheckBoxPreference import androidx.preference.EditTextPreference import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.multisrc.hiper.Hiper -import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga import keiyoushi.annotation.Source import keiyoushi.network.rateLimit -import keiyoushi.utils.getPreferences +import okhttp3.Headers +import okhttp3.OkHttpClient import okhttp3.Response @Source -abstract class Hiperdex : - Hiper(), - ConfigurableSource { +abstract class Hiperdex : Hiper() { - override fun headersBuilder() = super.headersBuilder() - .set("x-svc-gate", "f5pabmx7sdek") + override fun Headers.Builder.configureHeaders(): Headers.Builder = this + .set("x-cfg-auth", "yceqt7qgu004") - override val client = super.client.newBuilder() + override fun OkHttpClient.Builder.configureClient(): OkHttpClient.Builder = addHiperAuthInterceptor() .rateLimit(3) - .build() - - private val preferences = getPreferences() override fun setupPreferenceScreen(screen: PreferenceScreen) { + super.setupPreferenceScreen(screen) + val noRemoveTitleBrowsingPref = CheckBoxPreference(screen.context).apply { key = NO_REMOVE_TITLE_BROWSING_PREF title = "Don't apply title cleaning in browsing/search results" @@ -98,20 +95,20 @@ abstract class Hiperdex : screen.addPreference(noRemoveTitleBrowsingPref) } - override fun searchMangaParse(response: Response): MangasPage { - val (manga, hasNextPage) = super.searchMangaParse(response) + override fun parseSearchMangaList(response: Response): MangasPage { + val mangaUpdate = super.parseSearchMangaList(response) return MangasPage( - manga.map { + mangaUpdate.mangas.map { if (!noCleanTitlesWhileBrowsing()) { it.title = it.title.cleanTitleIfNeeded() } it }, - hasNextPage, + mangaUpdate.hasNextPage, ) } - override fun mangaDetailsParse(response: Response): SManga = super.mangaDetailsParse(response).apply { + override fun parseMangaDetails(response: Response): SManga = super.parseMangaDetails(response).apply { val cleanedTitle = title.cleanTitleIfNeeded() if (cleanedTitle != title.trim()) { description = listOfNotNull(title, description) @@ -150,4 +147,125 @@ abstract class Hiperdex : ) } } + + override val genresList = listOf( + "4-Koma", + "Action", + "Adaptation", + "Adult", + "Adventure", + "Age Gap", + "Aliens", + "Ancient Korea", + "Anthology", + "Campus", + "Childhood Friends", + "Comedy", + "Cooking", + "Crime", + "Crossdressing", + "Dance", + "Delinquents", + "Demons", + "Doujinshi", + "Drama", + "Ecchi", + "Escolar", + "Fantasy", + "Fellatio/Blowjob", + "Fetish", + "Full Color", + "Furry", + "Gender Bender", + "Genderswap", + "Ghosts", + "Girls' Love", + "Gore", + "Guideverse", + "Gyaru", + "Hair Color Change", + "Harem", + "Hentai", + "Heroes", + "Historical", + "Horror", + "Human-Nonhuman Relationship", + "Isekai", + "Josei", + "Korea", + "Korean Ambience", + "Korean BL", + "Long Strip", + "Long-Haired Male Character/s", + "Long-Haired Male Lead", + "Love Triangle/s", + "Low Fantasy", + "Maduro", + "Mafia", + "Magic", + "Male Protagonist", + "Manga", + "Martial Arts", + "Masculine Uke", + "Mature", + "Mecha", + "Medical", + "Military", + "Monster Girls", + "Monsters", + "Monsters Invade Earth", + "Murim", + "Muscular Male Lead", + "Muscular Uke", + "Music", + "Mystery", + "Nameverse", + "Ninja", + "Office Workers", + "Older Uke Younger Seme", + "Oneshot", + "Orphan Female Lead", + "Police", + "Post-Apocalyptic", + "Psychological", + "Red-Haired Male Lead", + "Red-Haired Seme", + "Regression", + "Reincarnation", + "Revenge", + "Romance", + "Samurai", + "School Life", + "Sci-fi", + "Secret Relationship", + "Seinen", + "Sexual Violence", + "Shota", + "Shoujo", + "Shoujo Ai", + "Shounen", + "Size Difference", + "Slice of Life", + "Smut", + "Sobrenatural", + "Sports", + "Superhero", + "Supernatural", + "Survival", + "Suspense", + "Thriller", + "Time Travel", + "Tower", + "Tragedy", + "Uncensored", + "Video Games", + "Villainess", + "Violence", + "Virtual Reality", + "Web Comic", + "Webtoon", + "Wuxia", + "Yaoi", + "Yuri", + ) } diff --git a/src/pt/hipercool/build.gradle.kts b/src/pt/hipercool/build.gradle.kts index 4d5d63ef4df..c30aa94a8e5 100644 --- a/src/pt/hipercool/build.gradle.kts +++ b/src/pt/hipercool/build.gradle.kts @@ -8,7 +8,7 @@ keiyoushi { name = "HipercooL" versionCode = 55 contentWarning = ContentWarning.NSFW - libVersion = "1.4" + libVersion = "1.6" theme = "hiper" source { diff --git a/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/Hipercool.kt b/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/Hipercool.kt index 4bc27be8ea0..fc41fd2e035 100644 --- a/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/Hipercool.kt +++ b/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/Hipercool.kt @@ -3,14 +3,538 @@ package eu.kanade.tachiyomi.extension.pt.hipercool import eu.kanade.tachiyomi.multisrc.hiper.Hiper import keiyoushi.annotation.Source import keiyoushi.network.rateLimit +import okhttp3.Headers +import okhttp3.OkHttpClient @Source abstract class Hipercool : Hiper() { - override fun headersBuilder() = super.headersBuilder() + override fun Headers.Builder.configureHeaders(): Headers.Builder = this .set("x-flux-node", "G2ZsDdWhUwdU82Vw") - override val client = super.client.newBuilder() + override fun OkHttpClient.Builder.configureClient(): OkHttpClient.Builder = addHiperAuthInterceptor() .rateLimit(3) - .build() + + override val genresList = listOf( + "Academia", + "Ação", + "Adult", + "Adulto", + "Adventure", + "Ahega", + "Ahegao", + "Akio takami", + "Amiga de infância", + "Amputado", + "Anal", + "Angelical", + "Animal", + "Anjo", + "Anjos", + "anorexic", + "Aprisionada", + "Ar livre", + "Aréola Grande", + "Arte Marcial", + "Artes Marciais", + "Artist CG", + "Ashikoki", + "Avental", + "Aventura", + "Bakuman", + "Bakunyuu", + "Bakuynuu", + "Banheiro", + "Banho", + "Banner", + "BBM", + "BBW", + "BDSM", + "Beatmania", + "Beauty Mark", + "Bêbada", + "Bêbado", + "Bêbado(a)", + "Bebida", + "Beijo grego", + "Bestial", + "bi", + "Big breasts", + "big penis", + "Bikini", + "Biquíni", + "bisexual", + "Bishoujo Senshi Sailor Moon", + "Bissexual", + "Bkaunyuu", + "Bleach", + "Bloomers", + "Blooomers", + "Blowjob", + "Bodysuit", + "Bonage", + "Bondage", + "Bongade", + "Brinquedos", + "Brinquedos Sexuais", + "Bronzeada", + "Bronzeado", + "Bronzeado(a)", + "Bronzeamento", + "Bruxa", + "BSDM", + "Bukakke", + "Bukkake", + "Bunda Grande", + "Bundão", + "Bunduda", + "BWC", + "Cabeluda", + "Calcinha", + "Camisinha", + "Camisola", + "Campione!", + "Campus", + "cannabis", + "Casada", + "Centauro", + "Centauro(a)", + "CEO", + "Chantagem", + "Chobits", + "Clannad", + "Clorofórmio", + "Clorofórmo", + "Club", + "Code Geass", + "Coelgial", + "Coelhinha", + "Coelhinho", + "Colã", + "Colegial", + "Colegual", + "Coleigal", + "Coleira", + "Colorida", + "Colorido", + "Comédia", + "Condage", + "Consolo", + "Controle da Mente", + "Controle Mental", + "Coprofilia", + "Corrupção", + "Cosplay", + "cosplaying", + "Creampie", + "Crosdressing", + "Cross-Dressing", + "Crossdresing", + "Crossdressing", + "Crossdressinh", + "Cunhada", + "Cuntboy", + "Dakr Skin", + "Dança", + "Dance In The Vampire Bund", + "Dark Skin", + "Darki Skin", + "Darkstalkers", + "Defloração", + "Demônio", + "Demônios", + "Dildo", + "DILF", + "Dokidoki! PreCure", + "Dormindo", + "Dragonaut", + "Drama", + "Dream C Club", + "Drogas", + "Drunk", + "Dupla Penetração", + "Ecchi", + "Elf", + "Elfa", + "Elfas", + "Elfo(a)", + "Elfos", + "Embriaguez", + "Empregada", + "Enema", + "Enfemeira", + "Enfermeira", + "Engravidando", + "Esapnhola", + "Escolar", + "Escrava", + "Escravidão", + "Escravo(a)", + "Espanhol", + "Espanhola", + "Esporte", + "Esportes", + "Esposa", + "Espsanhola", + "Esspanhola", + "Estrangeira", + "Exibicionismo", + "eye-covering bang", + "Facial", + "Familia", + "Fantasia", + "Fantasma", + "Fantasy", + "Fate/hollow ataraxia", + "Fate/stay night", + "Fecundação", + "Femdom", + "Femdom.Espanhola", + "feminization", + "Fetiche por Axila", + "Fetiches", + "Fezes", + "ffm threesome", + "Ficção", + "Ficcção", + "Filha", + "Filho", + "Filho(a)", + "Filho(a)Dupla Penetração", + "Filmagem", + "Filmando", + "Final Fantasy X-2", + "Final Fantasy XII", + "Final Fantasy XIII​", + "Fisting", + "Footjob", + "Freira", + "Fresh PreCure!", + "Full color", + "Furry", + "Futanari", + "Futanari com Futanari", + "Futanari on Futanari", + "Futanari on Futanarin", + "Futanari on Male", + "Gaping", + "Garçonete", + "Garganta Profunda", + "Gargantilha", + "Gargantilho", + "Garota Demônio", + "Garota Mágica", + "Garota Monstro", + "Garoto de sorte", + "Gêmeas", + "Gêmeos", + "Gêmeos(as)", + "Gender bender", + "Genderbend", + "Genshiken", + "Gigante", + "Gintama", + "Glasses", + "Goblin", + "Gordinha", + "Gordinho(a)", + "Gordinhoi(a)", + "Gozando Junto", + "Grávida", + "Gravidez", + "Gruapl", + "Grupal", + "Grupal.Incesto", + "GrupalIncesto", + "Grupla", + "Grupo", + "Gundam Build Fighters", + "Gundam Unicorn", + "Guri", + "Guro", + "Gyaru", + "H-Mangá", + "Happy Sex", + "Hardcore", + "Harém", + "Harry Potter", + "Hase Yuu", + "Hentai", + "Hikawa Yuuki", + "Hipnose", + "Historical", + "Histórico", + "Horror", + "Humilhação", + "Humor", + "Idols", + "Impregnação", + "Incesot", + "Incest", + "Incesto", + "Incesto.Crossdressing", + "inconsciente", + "Inseki", + "Inseto", + "Invisível", + "Irmã", + "Irmã mais velha", + "Irmã(ã)", + "Irmão", + "Irmão(ã)", + "Irmãos", + "Irmãs", + "Isekai", + "Jaleco", + "Jogos", + "Josei", + "Kemonomimi", + "Kimono", + "King of Fighters", + "kissing", + "Kogal", + "Kunoichi", + "Lactação", + "Lâmia", + "Latex", + "Lesbica", + "Líder de Torcida", + "Lingeire", + "Lingeri", + "Lingeria", + "Lingerie", + "Loja", + "Madrasta", + "Maduro", + "Mãe", + "Magia", + "Maid", + "Maiô", + "Maiô.Sumata", + "Mais", + "Mamilo Invertido", + "Marquinha", + "Masoquismo", + "Massagem", + "Masturabação", + "Masturbação", + "Mature", + "Meia", + "Meia Calça", + "Meia Creampie", + "Meia Longa", + "Meledom", + "Menage", + "Metrô", + "Miko", + "MILF", + "Militar", + "Mind Break", + "Mind Breka", + "Mind Creampie", + "Mistério", + "Mobile Suit Gundam Unicorn", + "Monstro", + "Monstros", + "Mordaça", + "Morena", + "Mosntro", + "Murim", + "Musculos", + "Mystery", + "Nakadashi", + "namorada", + "Necrofilia", + "Negras", + "Neko", + "Neotrare", + "Neto(a)", + "Netorare", + "Netorarea", + "Netori", + "Ninja", + "Noiva", + "Noivo(a)", + "NTR", + "Óculos", + "Onahole", + "Oni", + "Onsen", + "Oral", + "Orc", + "Orgasmo", + "Overlord", + "Oyakodon", + "Pai", + "Paizuri", + "Palmada", + "Paradise", + "Parto", + "Pau Grande", + "Peiõtes", + "Peitihnos", + "Peitinho", + "Peitinhos", + "PeitinhosTsundere", + "Peitõe", + "Peitões", + "Peitões.Primo(a)", + "Peitos Enormes", + "Peitos Grande", + "Peitos Grandes", + "Peittões", + "Pelos Pubianos", + "Penetração Dupla", + "Petiões", + "Piercing", + "Pintura Corporal", + "Po-ju", + "Policial", + "Ponytail", + "Possessão", + "Praia", + "Preservativo", + "Prima", + "Prima(o)", + "Primeira Vez", + "Primo", + "Primo(a)", + "Prisioneiro(a)", + "Profeesor(a)", + "Professor", + "Professor(a)", + "Professor(a)Oral", + "Prostitui;'ao", + "Prostituição", + "Prostituilção", + "prostitution", + "Psicológico", + "Raio-X", + "rape", + "Reencarnação", + "regressão de idade", + "Relacionamento secreto", + "Revenge", + "Robô Sexual", + "Romance", + "Ruiva", + "Sailor Suit", + "Scat", + "School girl", + "School Life", + "school uniform", + "Schoolboy uniform", + "Schoolgirl uniform", + "Sci-fi", + "Sedução", + "Seinen", + "Seios Grandes", + "Seios Pequenos", + "Sem Censura", + "Sensei", + "Sereia", + "sex stoy", + "Sex Toy", + "Sex Toys", + "Sexo em Público", + "Shimapan", + "Shota", + "Shoujo", + "Shoujo Ai", + "Shounen", + "Shounen Ai", + "Sinen", + "sissy", + "Slice of Life", + "Smut", + "Sobrenatural", + "Sobrevivência", + "Sobrinha", + "Sobrinho", + "Sobrinho(a)", + "Sobrinho(a)mFilha", + "sole female", + "sole male", + "Solo Female", + "Solo Male", + "sonho", + "Squirt", + "Squirting", + "Squrting", + "Stocking", + "Stockings", + "Strap-on", + "Strpa-on", + "Suamata", + "Succubus", + "Súcubo", + "Sumata", + "Supernatural", + "Swimsuit", + "Takese Yuu", + "Tamboy", + "tankoubon", + "Tapa-olho", + "Tatuagem na Virilha", + "Tentáculos", + "Terror", + "Tia", + "Tio", + "Tio(a)", + "Toddler", + "Toddlercon", + "tomboy", + "Tomgimrl", + "Tomgirl", + "Tomgirl (Gay)", + "Tomgirl Femdom", + "Tomrgirl", + "Tomrigl", + "Tortura", + "Tragédia", + "Traição", + "Transformação", + "Transformação de Sexo", + "Transsexual", + "Trap", + "Travesit", + "Travesti", + "Treinadora", + "Triação", + "Tribadism", + "Tribadismo", + "Troca de Corpos", + "Tsundere", + "Uniforme Escolar", + "Urina", + "Vampiro", + "Vanila", + "Vanilla", + "Velho", + "Velhote", + "Vellhote", + "Venda", + "Vida Cotidiana", + "Vida Escolar", + "Vingança", + "virgem", + "Viúva", + "Vore", + "Voyerismo", + "Voyeur", + "Voyeurismo", + "Voyeurismoão(ã)", + "Webtoon", + "Wolf Girl", + "Wuxia", + "X-Ray", + "Yamamoto Ryuusuke", + "Yandere", + "Yaoi", + "Yukata", + "Yuri", + "Zombie", + "Zoofilia", + "Zumbi", + ) } diff --git a/src/pt/hipertoon/build.gradle.kts b/src/pt/hipertoon/build.gradle.kts index df7cf85cd2d..22007322a25 100644 --- a/src/pt/hipertoon/build.gradle.kts +++ b/src/pt/hipertoon/build.gradle.kts @@ -8,7 +8,7 @@ keiyoushi { name = "Hipertoon" versionCode = 1 contentWarning = ContentWarning.MIXED - libVersion = "1.4" + libVersion = "1.6" theme = "hiper" source { diff --git a/src/pt/hipertoon/src/eu/kanade/tachiyomi/extension/pt/hipertoon/Hipertoon.kt b/src/pt/hipertoon/src/eu/kanade/tachiyomi/extension/pt/hipertoon/Hipertoon.kt index da81d67ce7f..0ba738e83a0 100644 --- a/src/pt/hipertoon/src/eu/kanade/tachiyomi/extension/pt/hipertoon/Hipertoon.kt +++ b/src/pt/hipertoon/src/eu/kanade/tachiyomi/extension/pt/hipertoon/Hipertoon.kt @@ -4,4 +4,100 @@ import eu.kanade.tachiyomi.multisrc.hiper.Hiper import keiyoushi.annotation.Source @Source -abstract class Hipertoon : Hiper() +abstract class Hipertoon : Hiper() { + + override val genresList = listOf( + "4-Koma", + "Ação", + "Adaptation", + "Adulto", + "Aliens", + "Animais", + "Antologia", + "Artes Marciais", + "Aventura", + "Boy's Love", + "Colorido Oficial", + "Comédia", + "Crime", + "Crossdressing", + "Culinária", + "Delinquentes", + "Demônios", + "Doujinshi", + "Drama", + "Ecchi", + "Escolar", + "Escritório", + "Esporte", + "Fan Colored", + "Fantasia", + "Fantasma", + "Ficção", + "Ficção Científica", + "Filosófico", + "Full Color", + "Gender Bender", + "Genderswap", + "Gore", + "Gyaru", + "Harém", + "Histórico", + "Horror", + "Incest", + "Isekai", + "Josei", + "Loli", + "Longstrip", + "Maduro", + "Máfia", + "Magia", + "Mecha", + "Médico", + "Militar", + "Mistério", + "Mitologia", + "Monster Girl", + "Monstros", + "Música", + "Ninja", + "One-Shot", + "Polícia", + "Pós-Apocalíptico", + "Premiado", + "Psicológico", + "Reencarnação", + "Regressão", + "Reverse Harem", + "Romance", + "Samurai", + "Seinen", + "Self-Published", + "Sexual Violence", + "Shota", + "Shotacon", + "Shoujo", + "Shoujo Ai", + "Shounen", + "Shounen Ai", + "Slice of Life", + "Smut", + "Sobrenatural", + "Sobrevivência", + "Super-Herói", + "Suspense", + "Terror", + "Traditional Games", + "Tragédia", + "Vampiros", + "Viagem no Tempo", + "Video Games", + "Vilã", + "Vingança", + "Web Comic", + "Wuxia", + "Yaoi", + "Yuri", + "Zumbi", + ) +}