diff --git a/src/en/toptoonplus/build.gradle b/src/en/toptoonplus/build.gradle index 135060cd9..12be9737f 100644 --- a/src/en/toptoonplus/build.gradle +++ b/src/en/toptoonplus/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'TOPTOON+' pkgNameSuffix = 'en.toptoonplus' extClass = '.TopToonPlus' - extVersionCode = 1 + extVersionCode = 2 isNsfw = true } diff --git a/src/en/toptoonplus/src/eu/kanade/tachiyomi/extension/en/toptoonplus/TopToonPlus.kt b/src/en/toptoonplus/src/eu/kanade/tachiyomi/extension/en/toptoonplus/TopToonPlus.kt index 5b0147ec4..e95db6de1 100644 --- a/src/en/toptoonplus/src/eu/kanade/tachiyomi/extension/en/toptoonplus/TopToonPlus.kt +++ b/src/en/toptoonplus/src/eu/kanade/tachiyomi/extension/en/toptoonplus/TopToonPlus.kt @@ -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>(response.body!!.string()) + val result = response.parseAs() 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>(response.body!!.string()) + val result = response.parseAs() if (result.data == null) { return MangasPage(emptyList(), hasNextPage = false) @@ -161,7 +161,7 @@ class TopToonPlus : HttpSource(), ConfigurableSource { return popularMangaParse(response) } - val result = json.decodeFromString>>(response.body!!.string()) + val result = response.parseAs>() 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>(response.body!!.string()) + val result = response.parseAs() 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 { - val result = json.decodeFromString>(response.body!!.string()) + val result = response.parseAs() 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 { - val result = json.decodeFromString>(response.body!!.string()) + val result = response.parseAs() 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() + + 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 = 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>(response.body!!.string()) + val result = response.parseAs() 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 Response.parseAs(): TopToonResult = 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) + } } } diff --git a/src/en/toptoonplus/src/eu/kanade/tachiyomi/extension/en/toptoonplus/TopToonPlusDto.kt b/src/en/toptoonplus/src/eu/kanade/tachiyomi/extension/en/toptoonplus/TopToonPlusDto.kt index e2d6a00ba..84b9670a4 100644 --- a/src/en/toptoonplus/src/eu/kanade/tachiyomi/extension/en/toptoonplus/TopToonPlusDto.kt +++ b/src/en/toptoonplus/src/eu/kanade/tachiyomi/extension/en/toptoonplus/TopToonPlusDto.kt @@ -22,13 +22,26 @@ data class TopToonDaily( data class TopToonDetails( val comic: TopToonComic? = null, val episode: List = emptyList() -) +) { + val availableEpisodes: List + 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 = emptyList() ) @@ -42,9 +55,22 @@ data class TopToonEpisodePrice( data class TopToonComic( val author: List = emptyList(), val comicId: Int = -1, + val hashtags: List = 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 )