comick: improve performance (#16947)

* comick: improve performance

for real this time

* simplify logic

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* remove redundancy

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
This commit is contained in:
AwkwardPeak7 2023-07-01 19:03:51 +05:00 committed by GitHub
parent 34483c9afe
commit fcba0e4efe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 162 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Comick' extName = 'Comick'
pkgNameSuffix = 'all.comickfun' pkgNameSuffix = 'all.comickfun'
extClass = '.ComickFunFactory' extClass = '.ComickFunFactory'
extVersionCode = 28 extVersionCode = 29
isNsfw = true isNsfw = true
} }

View File

@ -1,12 +1,8 @@
package eu.kanade.tachiyomi.extension.all.comickfun package eu.kanade.tachiyomi.extension.all.comickfun
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -17,20 +13,18 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import kotlin.math.min
abstract class ComickFun( abstract class ComickFun(
override val lang: String, override val lang: String,
private val comickFunLang: String, private val comickFunLang: String,
) : HttpSource(), ConfigurableSource { ) : HttpSource() {
override val name = "Comick" override val name = "Comick"
@ -47,62 +41,90 @@ abstract class ComickFun(
explicitNulls = true explicitNulls = true
} }
private lateinit var searchResponse: List<SearchManga>
override fun headersBuilder() = Headers.Builder().apply { override fun headersBuilder() = Headers.Builder().apply {
add("Referer", "$baseUrl/") add("Referer", "$baseUrl/")
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}") add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
} }
override val client: OkHttpClient = network.client.newBuilder() override val client = network.client.newBuilder()
.addNetworkInterceptor(::thumbnailIntercept) .addInterceptor(::thumbnailIntercept)
.rateLimit(4, 1) .rateLimit(3, 1)
.build() .build()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
/** Popular Manga **/ /** Popular Manga **/
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest( val url = "$apiUrl/v1.0/search?sort=follow&limit=$limit&page=$page&tachiyomi=true"
page = page, return GET(url, headers)
query = "",
filters = FilterList(
SortFilter("", getSortsList, defaultPopularSort),
),
)
} }
override fun popularMangaParse(response: Response) = searchMangaParse(response) override fun popularMangaParse(response: Response): MangasPage {
val result = response.parseAs<List<SearchManga>>()
return MangasPage(
result.map(SearchManga::toSManga),
hasNextPage = result.size >= limit,
)
}
/** Latest Manga **/ /** Latest Manga **/
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest( val url = "$apiUrl/v1.0/search?sort=uploaded&limit=$limit&page=$page&tachiyomi=true"
page = page, return GET(url, headers)
query = "",
filters = FilterList(
SortFilter("", getSortsList, defaultLatestSort),
),
)
} }
override fun latestUpdatesParse(response: Response) = searchMangaParse(response) override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
/** Manga Search **/ /** Manga Search **/
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (!query.startsWith(SLUG_SEARCH_PREFIX)) { return if (query.startsWith(SLUG_SEARCH_PREFIX)) {
return super.fetchSearchManga(page, query, filters) // url deep link
}
val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX) val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX)
val manga = SManga.create().apply { this.url = "/comic/$slugOrHid#" } val manga = SManga.create().apply { this.url = "/comic/$slugOrHid#" }
return fetchMangaDetails(manga).map { fetchMangaDetails(manga).map {
MangasPage(listOf(it), false) MangasPage(listOf(it), false)
} }
} else if (query.isEmpty()) {
// regular filtering without text search
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map(::searchMangaParse)
} else {
// text search, no pagination in api
if (page == 1) {
client.newCall(querySearchRequest(query))
.asObservableSuccess()
.map(::querySearchParse)
} else {
Observable.just(paginatedSearchPage(page))
}
}
}
private fun querySearchRequest(query: String): Request {
val url = "$apiUrl/v1.0/search?limit=300&page=1&tachiyomi=true"
.toHttpUrl().newBuilder()
.addQueryParameter("q", query.trim())
.build()
return GET(url, headers)
}
private fun querySearchParse(response: Response): MangasPage {
searchResponse = response.parseAs()
return paginatedSearchPage(1)
}
private fun paginatedSearchPage(page: Int): MangasPage {
val end = min(page * limit, searchResponse.size)
val entries = searchResponse.subList((page - 1) * limit, end)
.map(SearchManga::toSManga)
return MangasPage(entries, end < searchResponse.size)
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply { val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
if (query.isEmpty()) {
filters.forEach { it -> filters.forEach { it ->
when (it) { when (it) {
is CompletedFilter -> { is CompletedFilter -> {
@ -174,30 +196,15 @@ abstract class ComickFun(
else -> {} else -> {}
} }
} }
} else {
addQueryParameter("q", query)
}
addQueryParameter("tachiyomi", "true") addQueryParameter("tachiyomi", "true")
addQueryParameter("limit", "50") addQueryParameter("limit", "$limit")
addQueryParameter("page", "$page") addQueryParameter("page", "$page")
}.build() }.build()
return GET(url, headers) return GET(url, headers)
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response) = popularMangaParse(response)
val isQueryPresent = response.request.url.queryParameterNames.contains("q")
val result = response.parseAs<List<SearchManga>>()
return MangasPage(
result.map { it.toSManga(useScaledCover) },
/*
api always returns `limit` amount of results
for text search and page>=2 is always empty
so here we are checking if url has the text query parameter
to avoid false 'No result found' toasts.
*/
hasNextPage = !isQueryPresent && result.size >= 50,
)
}
/** Manga Details **/ /** Manga Details **/
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
@ -212,7 +219,7 @@ abstract class ComickFun(
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val mangaData = response.parseAs<Manga>() val mangaData = response.parseAs<Manga>()
return mangaData.toSManga(useScaledCover) return mangaData.toSManga()
} }
override fun getMangaUrl(manga: SManga): String { override fun getMangaUrl(manga: SManga): String {
@ -290,28 +297,11 @@ abstract class ComickFun(
throw UnsupportedOperationException("Not used") throw UnsupportedOperationException("Not used")
} }
protected open val defaultPopularSort: Int = 0
protected open val defaultLatestSort: Int = 4
override fun getFilterList() = getFilters() override fun getFilterList() = getFilters()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = coverQualityPref
title = "Cover Quality"
entries = arrayOf("Original", "Scaled")
entryValues = arrayOf("orig", "scaled")
setDefaultValue("orig")
summary = "%s"
}.let { screen.addPreference(it) }
}
private val useScaledCover: Boolean by lazy {
preferences.getString(coverQualityPref, "orig") != "orig"
}
companion object { companion object {
const val SLUG_SEARCH_PREFIX = "id:" const val SLUG_SEARCH_PREFIX = "id:"
private const val limit = 20
val dateFormat by lazy { val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH).apply { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC") timeZone = TimeZone.getTimeZone("UTC")
@ -320,6 +310,5 @@ abstract class ComickFun(
val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex() val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex()
val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex() val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex()
val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex() val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
private const val coverQualityPref = "pref_cover_quality"
} }
} }

View File

@ -13,11 +13,11 @@ data class SearchManga(
val cover_url: String? = null, val cover_url: String? = null,
) { ) {
fun toSManga(useScaledCover: Boolean) = SManga.create().apply { fun toSManga() = SManga.create().apply {
// appennding # at end as part of migration from slug to hid // appennding # at end as part of migration from slug to hid
url = "/comic/$hid#" url = "/comic/$hid#"
title = this@SearchManga.title title = this@SearchManga.title
thumbnail_url = parseCover(cover_url, md_covers, useScaledCover) thumbnail_url = parseCover(cover_url, md_covers)
} }
} }
@ -28,21 +28,24 @@ data class Manga(
val authors: List<Author> = emptyList(), val authors: List<Author> = emptyList(),
val genres: List<Genre> = emptyList(), val genres: List<Genre> = emptyList(),
) { ) {
fun toSManga(useScaledCover: Boolean) = SManga.create().apply { fun toSManga() = SManga.create().apply {
// appennding # at end as part of migration from slug to hid // appennding # at end as part of migration from slug to hid
url = "/comic/${comic.hid}#" url = "/comic/${comic.hid}#"
title = comic.title title = comic.title
description = comic.desc.beautifyDescription() description = comic.desc.beautifyDescription()
if (comic.altTitles.isNotEmpty()) { if (comic.altTitles.isNotEmpty()) {
description += comic.altTitles.joinToString( if (description.isNullOrEmpty()) {
separator = "\n", description = "Alternative Titles:\n"
prefix = "\n\nAlternative Titles:\n", } else {
) { description += "\n\nAlternative Titles:\n"
it.title.toString()
} }
description += comic.altTitles.mapNotNull { title ->
title.title?.let { "$it" }
}.joinToString("\n")
} }
status = comic.status.parseStatus(comic.translation_completed) status = comic.status.parseStatus(comic.translation_completed)
thumbnail_url = parseCover(comic.cover_url, comic.md_covers, useScaledCover) thumbnail_url = parseCover(comic.cover_url, comic.md_covers)
artist = artists.joinToString { it.name.trim() } artist = artists.joinToString { it.name.trim() }
author = authors.joinToString { it.name.trim() } author = authors.joinToString { it.name.trim() }
genre = genres.joinToString { it.name.trim() } genre = genres.joinToString { it.name.trim() }

View File

@ -171,7 +171,7 @@ private val getCreatedAtList: Array<Pair<String, String>> = arrayOf(
Pair("1 year", "365"), Pair("1 year", "365"),
) )
internal val getSortsList: Array<Pair<String, String>> = arrayOf( private val getSortsList: Array<Pair<String, String>> = arrayOf(
Pair("Most popular", "follow"), Pair("Most popular", "follow"),
Pair("Most follows", "user_follow_count"), Pair("Most follows", "user_follow_count"),
Pair("Most views", "view"), Pair("Most views", "view"),

View File

@ -11,6 +11,7 @@ import org.jsoup.parser.Parser
internal fun String.beautifyDescription(): String { internal fun String.beautifyDescription(): String {
return Parser.unescapeEntities(this, false) return Parser.unescapeEntities(this, false)
.substringBefore("---")
.replace(markdownLinksRegex, "") .replace(markdownLinksRegex, "")
.replace(markdownItalicBoldRegex, "") .replace(markdownItalicBoldRegex, "")
.replace(markdownItalicRegex, "") .replace(markdownItalicRegex, "")
@ -33,15 +34,11 @@ internal fun Int.parseStatus(translationComplete: Boolean): Int {
} }
} }
internal fun parseCover(thumbnailUrl: String?, mdCovers: List<MDcovers>, useScaled: Boolean): String? { internal fun parseCover(thumbnailUrl: String?, mdCovers: List<MDcovers>): String {
val b2key = runCatching { mdCovers.first().b2key } val b2key = runCatching { mdCovers.first().b2key }
.getOrNull() ?: "" .getOrNull() ?: ""
return if (useScaled) { return "$thumbnailUrl#$b2key"
"$thumbnailUrl#$b2key"
} else {
thumbnailUrl?.replaceAfterLast("/", b2key)
}
} }
internal fun thumbnailIntercept(chain: Interceptor.Chain): Response { internal fun thumbnailIntercept(chain: Interceptor.Chain): Response {