Add ability to read previously paid chapters in TTP (#10388)
* Add hability to read paid chapters in TTP. * Fix formating. * Remove promotional thumbnails.
This commit is contained in:
parent
fa7e1e75d0
commit
662bd7d1c5
|
@ -6,7 +6,7 @@ ext {
|
|||
extName = 'TOPTOON+'
|
||||
pkgNameSuffix = 'en.toptoonplus'
|
||||
extClass = '.TopToonPlus'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import android.app.Application
|
|||
import android.content.SharedPreferences
|
||||
import android.text.InputType
|
||||
import android.util.Base64
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
|
@ -36,7 +36,6 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
@ -81,6 +80,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
private val deviceId: String by lazy { UUID.randomUUID().toString() }
|
||||
|
||||
private var token: String? = null
|
||||
|
||||
private var userMature: Boolean = false
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
|
@ -97,7 +97,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = json.decodeFromString<TopToonResult<TopToonRanking>>(response.body!!.string())
|
||||
val result = response.parseAs<TopToonRanking>()
|
||||
|
||||
if (result.data == null) {
|
||||
return MangasPage(emptyList(), hasNextPage = false)
|
||||
|
@ -110,7 +110,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
|
||||
private fun popularMangaFromObject(comic: TopToonComic) = SManga.create().apply {
|
||||
title = comic.information?.title.orEmpty()
|
||||
thumbnail_url = comic.thumbnailImage?.jpeg?.firstOrNull()?.path.orEmpty()
|
||||
thumbnail_url = comic.firstAvailableThumbnail
|
||||
url = "/comic/${comic.comicId}"
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val result = json.decodeFromString<TopToonResult<TopToonDaily>>(response.body!!.string())
|
||||
val result = response.parseAs<TopToonDaily>()
|
||||
|
||||
if (result.data == null) {
|
||||
return MangasPage(emptyList(), hasNextPage = false)
|
||||
|
@ -161,7 +161,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
return popularMangaParse(response)
|
||||
}
|
||||
|
||||
val result = json.decodeFromString<TopToonResult<List<TopToonComic>>>(response.body!!.string())
|
||||
val result = response.parseAs<List<TopToonComic>>()
|
||||
|
||||
if (result.data == null) {
|
||||
return MangasPage(emptyList(), hasNextPage = false)
|
||||
|
@ -203,7 +203,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
||||
val result = json.decodeFromString<TopToonResult<TopToonDetails>>(response.body!!.string())
|
||||
val result = response.parseAs<TopToonDetails>()
|
||||
|
||||
if (result.data == null) {
|
||||
throw Exception(COULD_NOT_PARSE_RESPONSE)
|
||||
|
@ -212,23 +212,23 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
val comic = result.data.comic!!
|
||||
|
||||
title = comic.information?.title.orEmpty()
|
||||
thumbnail_url = comic.thumbnailImage?.jpeg?.firstOrNull()?.path.orEmpty()
|
||||
thumbnail_url = comic.firstAvailableThumbnail
|
||||
description = comic.information?.description
|
||||
status = SManga.ONGOING
|
||||
author = comic.author.joinToString(", ") { it.trim() }
|
||||
genre = comic.genres
|
||||
status = if (result.data.isCompleted) SManga.COMPLETED else SManga.ONGOING
|
||||
author = comic.author.joinToString { it.trim() }
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga.url)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val result = json.decodeFromString<TopToonResult<TopToonDetails>>(response.body!!.string())
|
||||
val result = response.parseAs<TopToonDetails>()
|
||||
|
||||
if (result.data == null) {
|
||||
throw Exception(COULD_NOT_PARSE_RESPONSE)
|
||||
}
|
||||
|
||||
return result.data.episode
|
||||
.filter { episode -> episode.information?.payType == 0 }
|
||||
return result.data.availableEpisodes
|
||||
.map(::chapterFromObject)
|
||||
.reversed()
|
||||
}
|
||||
|
@ -266,7 +266,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result = json.decodeFromString<TopToonResult<TopToonUsableEpisode>>(response.body!!.string())
|
||||
val result = response.parseAs<TopToonUsableEpisode>()
|
||||
|
||||
if (result.data == null) {
|
||||
throw Exception(COULD_NOT_PARSE_RESPONSE)
|
||||
|
@ -274,17 +274,32 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
|
||||
val usableEpisode = result.data
|
||||
|
||||
if (usableEpisode.isFree.not() ||
|
||||
usableEpisode.episodePrice?.payType != 0 ||
|
||||
usableEpisode.purchaseMethod.firstOrNull() != "FREE_EPISODE"
|
||||
) {
|
||||
if (usableEpisode.isFree.not() && usableEpisode.isOwn.not()) {
|
||||
throw Exception(CHAPTER_NOT_FREE)
|
||||
}
|
||||
|
||||
return usableEpisode.episode!!.contentImage?.jpeg.orEmpty()
|
||||
.mapIndexed { i, page ->
|
||||
Page(i, baseUrl, page.path)
|
||||
}
|
||||
val viewerRequest = viewerRequest(usableEpisode.comicId, usableEpisode.episodeId)
|
||||
val viewerResponse = client.newCall(viewerRequest).execute()
|
||||
val viewerResult = viewerResponse.parseAs<TopToonDetails>()
|
||||
|
||||
return viewerResult.data!!.episode
|
||||
.find { episode -> episode.episodeId == usableEpisode.episodeId }
|
||||
.let { episode -> episode?.contentImage?.jpeg.orEmpty() }
|
||||
.mapIndexed { i, page -> Page(i, baseUrl, page.path) }
|
||||
}
|
||||
|
||||
private fun viewerRequest(comicId: Int, episodeId: Int): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.add("Accept", ACCEPT_JSON)
|
||||
.add("X-Api-Key", API_KEY)
|
||||
.build()
|
||||
|
||||
val apiUrl = "$API_URL/api/v1/page/viewer".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("comicId", comicId.toString())
|
||||
.addQueryParameter("episodeId", episodeId.toString())
|
||||
.toString()
|
||||
|
||||
return GET(apiUrl, newHeaders)
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
||||
|
@ -308,6 +323,10 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
summary = EMAIL_PREF_SUMMARY
|
||||
dialogTitle = EMAIL_PREF_TITLE
|
||||
|
||||
setOnBindEditTextListener {
|
||||
it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
token = null
|
||||
|
||||
|
@ -337,7 +356,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
}
|
||||
}
|
||||
|
||||
val maturePref = CheckBoxPreference(screen.context).apply {
|
||||
val maturePref = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = MATURE_PREF_KEY
|
||||
title = MATURE_PREF_TITLE
|
||||
setDefaultValue(MATURE_PREF_DEFAULT)
|
||||
|
@ -377,11 +396,13 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
}
|
||||
|
||||
val newRequest = chain.request().newBuilder()
|
||||
.removeHeader("Token")
|
||||
.addHeader("Token", token.orEmpty().ifEmpty { "null" })
|
||||
.build()
|
||||
|
||||
return chain.proceed(newRequest)
|
||||
if (token.orEmpty().isNotEmpty()) {
|
||||
newRequest.removeHeader("Token")
|
||||
.addHeader("Token", token!!)
|
||||
}
|
||||
|
||||
return chain.proceed(newRequest.build())
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
|
@ -413,7 +434,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
throw IOException(COULD_NOT_LOGIN)
|
||||
}
|
||||
|
||||
val result = json.decodeFromString<TopToonResult<TopToonAuth>>(response.body!!.string())
|
||||
val result = response.parseAs<TopToonAuth>()
|
||||
|
||||
if (result.data == null) {
|
||||
throw IOException(COULD_NOT_LOGIN)
|
||||
|
@ -443,16 +464,18 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
return POST("$API_URL/users/setUser", newHeaders, requestBody, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): TopToonResult<T> = use {
|
||||
json.decodeFromString(it.body?.string().orEmpty())
|
||||
}
|
||||
|
||||
private fun String.toDate(): Long {
|
||||
return try {
|
||||
DATE_FORMATTER.parse(this)?.time ?: 0L
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
return runCatching { DATE_FORMATTER.parse(this)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val API_URL = "https://api.toptoonplus.com"
|
||||
|
||||
private val API_KEY by lazy {
|
||||
Base64.decode("U1VQRVJDT09MQVBJS0VZMjAyMSNAIyg=", Base64.DEFAULT)
|
||||
.toString(charset("UTF-8"))
|
||||
|
@ -480,6 +503,8 @@ class TopToonPlus : HttpSource(), ConfigurableSource {
|
|||
private const val MATURE_PREF_SUMMARY = "This setting only takes effect if you are signed in."
|
||||
private const val MATURE_PREF_DEFAULT = false
|
||||
|
||||
private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,13 +22,26 @@ data class TopToonDaily(
|
|||
data class TopToonDetails(
|
||||
val comic: TopToonComic? = null,
|
||||
val episode: List<TopToonEpisode> = emptyList()
|
||||
)
|
||||
) {
|
||||
val availableEpisodes: List<TopToonEpisode>
|
||||
get() = episode.filter { it.information?.payType == 0 || it.isPurchased == 1 }
|
||||
|
||||
val isCompleted: Boolean
|
||||
get() = episode.lastOrNull()?.information
|
||||
?.let {
|
||||
it.title.contains("[End]", true) ||
|
||||
it.subTitle.contains("[End]", true)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TopToonUsableEpisode(
|
||||
val comicId: Int = 0,
|
||||
val episode: TopToonEpisode? = null,
|
||||
val episodeId: Int = 0,
|
||||
val episodePrice: TopToonEpisodePrice? = null,
|
||||
val isFree: Boolean = false,
|
||||
val isOwn: Boolean = false,
|
||||
val needLogin: Boolean = false,
|
||||
val purchaseMethod: List<String> = emptyList()
|
||||
)
|
||||
|
@ -42,9 +55,22 @@ data class TopToonEpisodePrice(
|
|||
data class TopToonComic(
|
||||
val author: List<String> = emptyList(),
|
||||
val comicId: Int = -1,
|
||||
val hashtags: List<String> = emptyList(),
|
||||
val information: TopToonComicInfo? = null,
|
||||
val thumbnailImage: TopToonComicPoster? = null,
|
||||
)
|
||||
val titleVerticalThumbnail: TopToonComicPoster? = null
|
||||
) {
|
||||
val firstAvailableThumbnail: String
|
||||
get() = titleVerticalThumbnail?.jpeg?.firstOrNull()?.path
|
||||
?: thumbnailImage!!.jpeg.firstOrNull()?.path.orEmpty()
|
||||
|
||||
val genres: String
|
||||
get() = hashtags
|
||||
.flatMap { it.split("&") }
|
||||
.map(String::trim)
|
||||
.sorted()
|
||||
.joinToString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TopToonComicInfo(
|
||||
|
@ -69,6 +95,7 @@ data class TopToonEpisode(
|
|||
val contentImage: TopToonComicPoster? = null,
|
||||
val episodeId: Int = -1,
|
||||
val information: TopToonEpisodeInfo? = null,
|
||||
val isPurchased: Int = 0,
|
||||
val order: Int = -1
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue