From a336be5ccae591caeb326a1033c8bfbe4533f8f6 Mon Sep 17 00:00:00 2001 From: Arraiment <76941874+Arraiment@users.noreply.github.com> Date: Fri, 20 Aug 2021 22:41:59 +0800 Subject: [PATCH] Add mature comics and other changes for Tapas.io (#8672) * Migration to kotlinx.serialization * Add support for mature results * Changed preferences implementation --- src/en/tapastic/build.gradle | 3 +- .../extension/en/tapastic/Tapastic.kt | 233 ++++++++++++------ 2 files changed, 162 insertions(+), 74 deletions(-) diff --git a/src/en/tapastic/build.gradle b/src/en/tapastic/build.gradle index 5e128e00a..84d5339f5 100644 --- a/src/en/tapastic/build.gradle +++ b/src/en/tapastic/build.gradle @@ -1,11 +1,12 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' ext { extName = 'Tapas' pkgNameSuffix = 'en.tapastic' extClass = '.Tapastic' - extVersionCode = 11 + extVersionCode = 12 libVersion = '1.2' } diff --git a/src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt b/src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt index 2bff2261d..e200ed194 100644 --- a/src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt +++ b/src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt @@ -2,14 +2,8 @@ package eu.kanade.tachiyomi.extension.en.tapastic import android.app.Application import android.content.SharedPreferences -import android.net.Uri -import com.github.salomonbrys.kotson.bool -import com.github.salomonbrys.kotson.fromJson -import com.github.salomonbrys.kotson.get -import com.github.salomonbrys.kotson.int -import com.github.salomonbrys.kotson.string -import com.google.gson.Gson -import com.google.gson.JsonObject +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.Filter @@ -19,7 +13,17 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.Cookie +import okhttp3.CookieJar import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import org.jsoup.Jsoup @@ -27,64 +31,52 @@ import org.jsoup.nodes.Document import org.jsoup.nodes.Element import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale class Tapastic : ConfigurableSource, ParsedHttpSource() { - // Preferences Code + // Preferences private val preferences: SharedPreferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } - override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) { - val chapterListPref = androidx.preference.ListPreference(screen.context).apply { - key = SHOW_LOCKED_CHAPTERS_Title - title = SHOW_LOCKED_CHAPTERS_Title - entries = prefsEntriesChapters - entryValues = prefsEntryValuesChapters - summary = "%s" + override fun setupPreferenceScreen(screen: PreferenceScreen) { + val chapterVisibilityPref = SwitchPreferenceCompat(screen.context).apply { + key = CHAPTER_VIS_PREF_KEY + title = "Show paywalled chapters" + summary = "Tapas requires login/payment for some chapters. Enable to always show paywalled chapters." + setDefaultValue(true) setOnPreferenceChangeListener { _, newValue -> - val selected = newValue as String - val index = this.findIndexOfValue(selected) - val entry = entryValues[index] as String - preferences.edit().putString(SHOW_LOCKED_CHAPTERS, entry).commit() + val checkValue = newValue as Boolean + preferences.edit().putBoolean(CHAPTER_VIS_PREF_KEY, checkValue).commit() } } - screen.addPreference(chapterListPref) + screen.addPreference(chapterVisibilityPref) - val lockPref = androidx.preference.ListPreference(screen.context).apply { - key = SHOW_LOCK_Title - title = SHOW_LOCK_Title - entries = prefsEntriesLock - entryValues = prefsEntryValuesLock - summary = "%s" + val lockPref = SwitchPreferenceCompat(screen.context).apply { + key = SHOW_LOCK_PREF_KEY + title = "Show lock icon" + summary = "Enable to continue showing \uD83D\uDD12 for locked chapters after login." + setDefaultValue(false) setOnPreferenceChangeListener { _, newValue -> - val selected = newValue as String - val index = this.findIndexOfValue(selected) - val entry = entryValues[index] as String - preferences.edit().putString(SHOW_LOCK, entry).commit() + val checkValue = newValue as Boolean + preferences.edit().putBoolean(SHOW_LOCK_PREF_KEY, checkValue).commit() } } screen.addPreference(lockPref) } - private fun chapterListPref() = preferences.getString(SHOW_LOCKED_CHAPTERS, "free") - private fun lockPref() = preferences.getString(SHOW_LOCK, "yes") + private fun showLockedChapterPref() = preferences.getBoolean(CHAPTER_VIS_PREF_KEY, false) + private fun showLockPref() = preferences.getBoolean(SHOW_LOCK_PREF_KEY, false) companion object { - private const val SHOW_LOCKED_CHAPTERS_Title = "Tapas requires login/payment for some chapters" - private const val SHOW_LOCKED_CHAPTERS = "tapas_locked_chapters" - private val prefsEntriesChapters = arrayOf("Show all chapters (including pay-to-read)", "Only show free chapters") - private val prefsEntryValuesChapters = arrayOf("all", "free") - - private const val SHOW_LOCK_Title = "Show \uD83D\uDD12 for locked chapters after login" - private const val SHOW_LOCK = "tapas_lock" - private val prefsEntriesLock = arrayOf("Yes", "No") - private val prefsEntryValuesLock = arrayOf("yes", "no") + private const val CHAPTER_VIS_PREF_KEY = "lockedChapterVisibility" + private const val SHOW_LOCK_PREF_KEY = "showChapterLock" } // Info @@ -94,6 +86,34 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { override val baseUrl = "https://tapas.io" override val id = 3825434541981130345 + override val client: OkHttpClient = super.client.newBuilder() + .cookieJar( + object : CookieJar { + override fun saveFromResponse(url: HttpUrl, cookies: List) {} + override fun loadForRequest(url: HttpUrl): MutableList { + return ArrayList().apply { + add( + Cookie.Builder() + .domain("tapas.io") + .path("/") + .name("birthDate") + .value("1994-01-01") + .build() + ) + add( + Cookie.Builder() + .domain("tapas.io") + .path("/") + .name("adjustedBirthDate") + .value("1994-01-01") + .build() + ) + } + } + } + ) + .build() + override fun headersBuilder(): Headers.Builder = Headers.Builder() .add("Referer", "https://m.tapas.io") @@ -123,24 +143,35 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { // Search override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - // If there is any search text, use text search, otherwise use filter search - val uri = if (query.isNotBlank()) { - Uri.parse("$baseUrl/search") - .buildUpon() - .appendQueryParameter("t", "COMICS") - .appendQueryParameter("q", query) + val filterList = if (filters.isEmpty()) getFilterList() else filters + val url: HttpUrl.Builder + // If there is any search text, use text search, ignoring filters + if (query.isNotBlank()) { + url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("q", query) + .addQueryParameter("t", "COMICS") } else { - val uri = Uri.parse("$baseUrl/comics").buildUpon() - // Append uri filters - filters.forEach { - if (it is UriFilter) - it.addToUri(uri) + // Checking mature filter + val matureFilter = filterList.find { it is MatureFilter } as MatureFilter + if (matureFilter.state) { + url = "$baseUrl/mature".toHttpUrlOrNull()!!.newBuilder() + // Append only mature uri filters + filterList.forEach { + if (it is UriFilter && it.isMature) + it.addToUri(url) + } + } else { + url = "$baseUrl/comics".toHttpUrlOrNull()!!.newBuilder() + // Append only non-mature uri filters + filterList.forEach { + if (it is UriFilter && !it.isMature) + it.addToUri(url) + } } - uri } // Append page number - uri.appendQueryParameter("pageNumber", page.toString()) - return GET(uri.toString()) + url.addQueryParameter("pageNumber", page.toString()) + return GET(url.toString()) } override fun searchMangaNextPageSelector() = @@ -156,7 +187,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { // Details override fun mangaDetailsRequest(manga: SManga): Request { - return GET(baseUrl + "${manga.url}/info") + return GET(baseUrl + "${manga.url}/info", headers) } override fun mangaDetailsParse(document: Document) = SManga.create().apply { @@ -174,10 +205,10 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { * Checklist: Paginated chapter lists, locked chapters, future chapters, early-access chapters (app only?), chapter order */ - private val gson by lazy { Gson() } + private val json: Json by injectLazy() private fun Element.isLockedChapter(): Boolean { - return this.hasClass("js-have-to-sign") || (lockPref() == "yes" && this.hasClass("js-locked")) + return this.hasClass("js-have-to-sign") || (showLockPref() && this.hasClass("js-locked")) } override fun chapterListParse(response: Response): List { @@ -188,16 +219,19 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { // recursively build the chapter list fun parseChapters(page: Int) { val url = "$baseUrl/series/$mangaId/episodes?page=$page&sort=NEWEST&init_load=0&large=true&last_access=0&" - val json = gson.fromJson(client.newCall(GET(url, headers)).execute().body!!.string())["data"] + val jsonResponse = client.newCall(GET(url, headers)).execute() + val json = json.parseToJsonElement(jsonResponse.body!!.string()).jsonObject["data"]!!.jsonObject - Jsoup.parse(json["body"].string).select(chapterListSelector()) + Jsoup.parse(json["body"]!!.jsonPrimitive.content).select(chapterListSelector()) .let { list -> // show/don't show locked chapters based on user's preferences - if (chapterListPref() == "free") list.filterNot { it.isLockedChapter() } else list + if (showLockedChapterPref()) list else list.filterNot { it.isLockedChapter() } } .map { chapters.add(chapterFromElement(it)) } - if (json["pagination"]["has_next"].bool) parseChapters(json["pagination"]["page"].int) + val hasNextPage = json["pagination"]!!.jsonObject["has_next"]!!.jsonPrimitive.boolean + val nextPage = json["pagination"]!!.jsonObject["page"]!!.jsonPrimitive.int + if (hasNextPage) parseChapters(nextPage) } parseChapters(1) @@ -236,21 +270,28 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { override fun getFilterList() = FilterList( // Tapastic does not support genre filtering and text search at the same time - Filter.Header("NOTE: Ignored if using text search!"), + Filter.Header("NOTE: All filters ignored if using text search!"), Filter.Separator(), - FilterFilter(), + CategoryFilter(), GenreFilter(), StatusFilter(), + Filter.Header("Sort is ignored when category filter is active!"), + SortFilter(), Filter.Separator(), - Filter.Header("Sort is ignored when filter is active!"), - SortFilter() + Filter.Header("Mature filters"), + MatureFilter("Show Mature Results Only"), + MatureCategoryFilter(), + MatureGenreFilter(), + Filter.Header("Sort is ignored when category filter is active!"), + MatureSortFilter(), ) - private class FilterFilter : UriSelectFilter( - "Filter", + private class CategoryFilter : UriSelectFilter( + "Category", + false, "b", arrayOf( - Pair("ALL", "None"), + Pair("ALL", "All"), Pair("POPULAR", "Popular"), Pair("TRENDING", "Trending"), Pair("FRESH", "Fresh"), @@ -263,6 +304,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { private class GenreFilter : UriSelectFilter( "Genre", + false, "g", arrayOf( Pair("0", "Any"), @@ -284,6 +326,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { private class StatusFilter : UriSelectFilter( "Status", + false, "f", arrayOf( Pair("NONE", "All"), @@ -294,6 +337,48 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { private class SortFilter : UriSelectFilter( "Sort", + false, + "s", + arrayOf( + Pair("DATE", "Date"), + Pair("LIKE", "Likes"), + Pair("SUBSCRIBE", "Subscribers") + ) + ) + + private class MatureFilter(name: String) : Filter.CheckBox(name) + + private class MatureCategoryFilter : UriSelectFilter( + "Category", + true, + "b", + arrayOf( + Pair("ALL", "All"), + Pair("POPULAR", "Popular"), + Pair("FRESH", "Fresh"), + ), + firstIsUnspecified = false, + defaultValue = 1 + ) + + private class MatureGenreFilter : UriSelectFilter( + "Genre", + false, + "g", + arrayOf( + Pair("0", "Any"), + Pair("5", "Romance"), + Pair("8", "Drama"), + Pair("22", "Boys Love"), + Pair("24", "Girls Love"), + Pair("2", "Comedy"), + Pair("6", "Horror"), + ) + ) + + private class MatureSortFilter : UriSelectFilter( + "Sort", + true, "s", arrayOf( Pair("DATE", "Date"), @@ -310,6 +395,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { // vals: private open class UriSelectFilter( displayName: String, + override val isMature: Boolean, val uriParam: String, val vals: Array>, val firstIsUnspecified: Boolean = true, @@ -317,9 +403,9 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { ) : Filter.Select(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter { - override fun addToUri(uri: Uri.Builder) { + override fun addToUri(uri: HttpUrl.Builder) { if (state != 0 || !firstIsUnspecified) - uri.appendQueryParameter(uriParam, vals[state].first) + uri.addQueryParameter(uriParam, vals[state].first) } } @@ -327,6 +413,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() { * Represents a filter that is able to modify a URI. */ private interface UriFilter { - fun addToUri(uri: Uri.Builder) + val isMature: Boolean + fun addToUri(uri: HttpUrl.Builder) } }