Add mature comics and other changes for Tapas.io (#8672)

* Migration to kotlinx.serialization

* Add support for mature results

* Changed preferences implementation
This commit is contained in:
Arraiment 2021-08-20 22:41:59 +08:00 committed by GitHub
parent d09c4a0abb
commit a336be5cca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 162 additions and 74 deletions

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Tapas' extName = 'Tapas'
pkgNameSuffix = 'en.tapastic' pkgNameSuffix = 'en.tapastic'
extClass = '.Tapastic' extClass = '.Tapastic'
extVersionCode = 11 extVersionCode = 12
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -2,14 +2,8 @@ package eu.kanade.tachiyomi.extension.en.tapastic
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import androidx.preference.PreferenceScreen
import com.github.salomonbrys.kotson.bool import androidx.preference.SwitchPreferenceCompat
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 eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter 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.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup 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.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
@ -27,64 +31,52 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class Tapastic : ConfigurableSource, ParsedHttpSource() { class Tapastic : ConfigurableSource, ParsedHttpSource() {
// Preferences Code // Preferences
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val chapterListPref = androidx.preference.ListPreference(screen.context).apply { val chapterVisibilityPref = SwitchPreferenceCompat(screen.context).apply {
key = SHOW_LOCKED_CHAPTERS_Title key = CHAPTER_VIS_PREF_KEY
title = SHOW_LOCKED_CHAPTERS_Title title = "Show paywalled chapters"
entries = prefsEntriesChapters summary = "Tapas requires login/payment for some chapters. Enable to always show paywalled chapters."
entryValues = prefsEntryValuesChapters setDefaultValue(true)
summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String val checkValue = newValue as Boolean
val index = this.findIndexOfValue(selected) preferences.edit().putBoolean(CHAPTER_VIS_PREF_KEY, checkValue).commit()
val entry = entryValues[index] as String
preferences.edit().putString(SHOW_LOCKED_CHAPTERS, entry).commit()
} }
} }
screen.addPreference(chapterListPref) screen.addPreference(chapterVisibilityPref)
val lockPref = androidx.preference.ListPreference(screen.context).apply { val lockPref = SwitchPreferenceCompat(screen.context).apply {
key = SHOW_LOCK_Title key = SHOW_LOCK_PREF_KEY
title = SHOW_LOCK_Title title = "Show lock icon"
entries = prefsEntriesLock summary = "Enable to continue showing \uD83D\uDD12 for locked chapters after login."
entryValues = prefsEntryValuesLock setDefaultValue(false)
summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String val checkValue = newValue as Boolean
val index = this.findIndexOfValue(selected) preferences.edit().putBoolean(SHOW_LOCK_PREF_KEY, checkValue).commit()
val entry = entryValues[index] as String
preferences.edit().putString(SHOW_LOCK, entry).commit()
} }
} }
screen.addPreference(lockPref) screen.addPreference(lockPref)
} }
private fun chapterListPref() = preferences.getString(SHOW_LOCKED_CHAPTERS, "free") private fun showLockedChapterPref() = preferences.getBoolean(CHAPTER_VIS_PREF_KEY, false)
private fun lockPref() = preferences.getString(SHOW_LOCK, "yes") private fun showLockPref() = preferences.getBoolean(SHOW_LOCK_PREF_KEY, false)
companion object { companion object {
private const val SHOW_LOCKED_CHAPTERS_Title = "Tapas requires login/payment for some chapters" private const val CHAPTER_VIS_PREF_KEY = "lockedChapterVisibility"
private const val SHOW_LOCKED_CHAPTERS = "tapas_locked_chapters" private const val SHOW_LOCK_PREF_KEY = "showChapterLock"
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")
} }
// Info // Info
@ -94,6 +86,34 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
override val baseUrl = "https://tapas.io" override val baseUrl = "https://tapas.io"
override val id = 3825434541981130345 override val id = 3825434541981130345
override val client: OkHttpClient = super.client.newBuilder()
.cookieJar(
object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
return ArrayList<Cookie>().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() override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", "https://m.tapas.io") .add("Referer", "https://m.tapas.io")
@ -123,24 +143,35 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
// Search // Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// If there is any search text, use text search, otherwise use filter search val filterList = if (filters.isEmpty()) getFilterList() else filters
val uri = if (query.isNotBlank()) { val url: HttpUrl.Builder
Uri.parse("$baseUrl/search") // If there is any search text, use text search, ignoring filters
.buildUpon() if (query.isNotBlank()) {
.appendQueryParameter("t", "COMICS") url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder()
.appendQueryParameter("q", query) .addQueryParameter("q", query)
.addQueryParameter("t", "COMICS")
} else { } else {
val uri = Uri.parse("$baseUrl/comics").buildUpon() // Checking mature filter
// Append uri filters val matureFilter = filterList.find { it is MatureFilter } as MatureFilter
filters.forEach { if (matureFilter.state) {
if (it is UriFilter) url = "$baseUrl/mature".toHttpUrlOrNull()!!.newBuilder()
it.addToUri(uri) // 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 // Append page number
uri.appendQueryParameter("pageNumber", page.toString()) url.addQueryParameter("pageNumber", page.toString())
return GET(uri.toString()) return GET(url.toString())
} }
override fun searchMangaNextPageSelector() = override fun searchMangaNextPageSelector() =
@ -156,7 +187,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
// Details // Details
override fun mangaDetailsRequest(manga: SManga): Request { 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 { 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 * 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 { 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<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
@ -188,16 +219,19 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
// recursively build the chapter list // recursively build the chapter list
fun parseChapters(page: Int) { fun parseChapters(page: Int) {
val url = "$baseUrl/series/$mangaId/episodes?page=$page&sort=NEWEST&init_load=0&large=true&last_access=0&" val url = "$baseUrl/series/$mangaId/episodes?page=$page&sort=NEWEST&init_load=0&large=true&last_access=0&"
val json = gson.fromJson<JsonObject>(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 -> .let { list ->
// show/don't show locked chapters based on user's preferences // 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)) } .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) parseChapters(1)
@ -236,21 +270,28 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
// Tapastic does not support genre filtering and text search at the same time // 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(), Filter.Separator(),
FilterFilter(), CategoryFilter(),
GenreFilter(), GenreFilter(),
StatusFilter(), StatusFilter(),
Filter.Header("Sort is ignored when category filter is active!"),
SortFilter(),
Filter.Separator(), Filter.Separator(),
Filter.Header("Sort is ignored when filter is active!"), Filter.Header("Mature filters"),
SortFilter() MatureFilter("Show Mature Results Only"),
MatureCategoryFilter(),
MatureGenreFilter(),
Filter.Header("Sort is ignored when category filter is active!"),
MatureSortFilter(),
) )
private class FilterFilter : UriSelectFilter( private class CategoryFilter : UriSelectFilter(
"Filter", "Category",
false,
"b", "b",
arrayOf( arrayOf(
Pair("ALL", "None"), Pair("ALL", "All"),
Pair("POPULAR", "Popular"), Pair("POPULAR", "Popular"),
Pair("TRENDING", "Trending"), Pair("TRENDING", "Trending"),
Pair("FRESH", "Fresh"), Pair("FRESH", "Fresh"),
@ -263,6 +304,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
private class GenreFilter : UriSelectFilter( private class GenreFilter : UriSelectFilter(
"Genre", "Genre",
false,
"g", "g",
arrayOf( arrayOf(
Pair("0", "Any"), Pair("0", "Any"),
@ -284,6 +326,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
private class StatusFilter : UriSelectFilter( private class StatusFilter : UriSelectFilter(
"Status", "Status",
false,
"f", "f",
arrayOf( arrayOf(
Pair("NONE", "All"), Pair("NONE", "All"),
@ -294,6 +337,48 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
private class SortFilter : UriSelectFilter( private class SortFilter : UriSelectFilter(
"Sort", "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", "s",
arrayOf( arrayOf(
Pair("DATE", "Date"), Pair("DATE", "Date"),
@ -310,6 +395,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
// vals: <name, display> // vals: <name, display>
private open class UriSelectFilter( private open class UriSelectFilter(
displayName: String, displayName: String,
override val isMature: Boolean,
val uriParam: String, val uriParam: String,
val vals: Array<Pair<String, String>>, val vals: Array<Pair<String, String>>,
val firstIsUnspecified: Boolean = true, val firstIsUnspecified: Boolean = true,
@ -317,9 +403,9 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
) : ) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue),
UriFilter { UriFilter {
override fun addToUri(uri: Uri.Builder) { override fun addToUri(uri: HttpUrl.Builder) {
if (state != 0 || !firstIsUnspecified) 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. * Represents a filter that is able to modify a URI.
*/ */
private interface UriFilter { private interface UriFilter {
fun addToUri(uri: Uri.Builder) val isMature: Boolean
fun addToUri(uri: HttpUrl.Builder)
} }
} }