Comick: fix new chapters delay and small refactor (#1354)
CI / Prepare job (push) Successful in 5s Details
CI / Build individual modules (push) Failing after 0s Details
CI / Publish repo (push) Has been skipped Details

* remove chapter pagination

page parameter seems to trigger some cache issue in their api

* update baseUrl

* data class -> class

micro optimization

* small refactor

* remove useless interceptor

* oops

* mutable not needed
This commit is contained in:
AwkwardPeak7 2024-02-18 16:09:05 +05:00 committed by Draff
parent 9fa6b8cb51
commit 9602aa5dd5
8 changed files with 75 additions and 118 deletions

View File

@ -3,7 +3,7 @@
<application> <application>
<activity <activity
android:name=".all.comickfun.ComickFunUrlActivity" android:name=".all.comickfun.ComickUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">
@ -14,6 +14,7 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="comick.io" />
<data android:host="comick.cc" /> <data android:host="comick.cc" />
<data android:host="comick.ink" /> <data android:host="comick.ink" />
<data android:host="comick.app" /> <data android:host="comick.app" />

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Comick' extName = 'Comick'
extClass = '.ComickFunFactory' extClass = '.ComickFactory'
extVersionCode = 41 extVersionCode = 42
isNsfw = true isNsfw = true
} }

View File

@ -26,19 +26,16 @@ import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import kotlin.math.min import kotlin.math.min
abstract class ComickFun( abstract class Comick(
override val lang: String, override val lang: String,
private val comickFunLang: String, private val comickLang: String,
) : ConfigurableSource, HttpSource() { ) : ConfigurableSource, HttpSource() {
override val name = "Comick" override val name = "Comick"
override val baseUrl = "https://comick.cc" override val baseUrl = "https://comick.io"
private val apiUrl = "https://api.comick.fun" private val apiUrl = "https://api.comick.fun"
@ -62,8 +59,9 @@ abstract class ComickFun(
) )
} }
private val preferences: SharedPreferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.newLineIgnoredGroups()
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
@ -151,23 +149,18 @@ abstract class ComickFun(
private val SharedPreferences.scorePosition: String private val SharedPreferences.scorePosition: String
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
init {
preferences.newLineIgnoredGroups()
}
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 = network.client.newBuilder() override val client = network.client.newBuilder()
.addInterceptor(::thumbnailIntercept)
.rateLimit(3, 1) .rateLimit(3, 1)
.build() .build()
/** Popular Manga **/ /** Popular Manga **/
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/v1.0/search?sort=follow&limit=$limit&page=$page&tachiyomi=true" val url = "$apiUrl/v1.0/search?sort=follow&limit=$LIMIT&page=$page&tachiyomi=true"
return GET(url, headers) return GET(url, headers)
} }
@ -175,13 +168,13 @@ abstract class ComickFun(
val result = response.parseAs<List<SearchManga>>() val result = response.parseAs<List<SearchManga>>()
return MangasPage( return MangasPage(
result.map(SearchManga::toSManga), result.map(SearchManga::toSManga),
hasNextPage = result.size >= limit, hasNextPage = result.size >= LIMIT,
) )
} }
/** Latest Manga **/ /** Latest Manga **/
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/v1.0/search?sort=uploaded&limit=$limit&page=$page&tachiyomi=true" val url = "$apiUrl/v1.0/search?sort=uploaded&limit=$LIMIT&page=$page&tachiyomi=true"
return GET(url, headers) return GET(url, headers)
} }
@ -233,8 +226,8 @@ abstract class ComickFun(
} }
private fun paginatedSearchPage(page: Int): MangasPage { private fun paginatedSearchPage(page: Int): MangasPage {
val end = min(page * limit, searchResponse.size) val end = min(page * LIMIT, searchResponse.size)
val entries = searchResponse.subList((page - 1) * limit, end) val entries = searchResponse.subList((page - 1) * LIMIT, end)
.map(SearchManga::toSManga) .map(SearchManga::toSManga)
return MangasPage(entries, end < searchResponse.size) return MangasPage(entries, end < searchResponse.size)
} }
@ -317,7 +310,7 @@ abstract class ComickFun(
} }
} }
addQueryParameter("tachiyomi", "true") addQueryParameter("tachiyomi", "true")
addQueryParameter("limit", "$limit") addQueryParameter("limit", "$LIMIT")
addQueryParameter("page", "$page") addQueryParameter("page", "$page")
}.build() }.build()
@ -367,7 +360,7 @@ abstract class ComickFun(
val coversUrl = val coversUrl =
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true" "$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
val covers = client.newCall(GET(coversUrl)).execute() val covers = client.newCall(GET(coversUrl)).execute()
.parseAs<Covers>().md_covers.reversed() .parseAs<Covers>().mdCovers.reversed()
return mangaData.toSManga( return mangaData.toSManga(
includeMuTags = preferences.includeMuTags, includeMuTags = preferences.includeMuTags,
covers = if (covers.any { it.vol == "1" }) covers.filter { it.vol == "1" } else covers, covers = if (covers.any { it.vol == "1" }) covers.filter { it.vol == "1" } else covers,
@ -387,19 +380,15 @@ abstract class ComickFun(
throw Exception("Migrate from Comick to Comick") throw Exception("Migrate from Comick to Comick")
} }
return paginatedChapterListRequest(manga.url.removeSuffix("#"), 1) val mangaUrl = manga.url.removeSuffix("#")
} val url = "$apiUrl$mangaUrl".toHttpUrl().newBuilder().apply {
addPathSegment("chapters")
if (comickLang != "all") addQueryParameter("lang", comickLang)
addQueryParameter("tachiyomi", "true")
addQueryParameter("limit", "$CHAPTERS_LIMIT")
}.build()
private fun paginatedChapterListRequest(mangaUrl: String, page: Int): Request { return GET(url, headers)
return GET(
"$apiUrl$mangaUrl".toHttpUrl().newBuilder().apply {
addPathSegment("chapters")
if (comickFunLang != "all") addQueryParameter("lang", comickFunLang)
addQueryParameter("tachiyomi", "true")
addQueryParameter("page", "$page")
}.build(),
headers,
)
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
@ -409,20 +398,6 @@ abstract class ComickFun(
.substringBefore("/chapters") .substringBefore("/chapters")
.substringAfter(apiUrl) .substringAfter(apiUrl)
var resultSize = chapterListResponse.chapters.size
var page = 2
while (chapterListResponse.total > resultSize) {
val newRequest = paginatedChapterListRequest(mangaUrl, page)
val newResponse = client.newCall(newRequest).execute()
val newChapterListResponse = newResponse.parseAs<ChapterList>()
chapterListResponse.chapters += newChapterListResponse.chapters
resultSize += newChapterListResponse.chapters.size
page += 1
}
return chapterListResponse.chapters return chapterListResponse.chapters
.filter { .filter {
it.groups.map { g -> g.lowercase() }.intersect(preferences.ignoredGroups).isEmpty() it.groups.map { g -> g.lowercase() }.intersect(preferences.ignoredGroups).isEmpty()
@ -457,8 +432,8 @@ abstract class ComickFun(
override fun getFilterList() = getFilters() override fun getFilterList() = getFilters()
private fun SharedPreferences.newLineIgnoredGroups() { private fun SharedPreferences.newLineIgnoredGroups(): SharedPreferences {
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return this
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty() val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
edit() edit()
@ -472,6 +447,8 @@ abstract class ComickFun(
) )
.putBoolean(MIGRATED_IGNORED_GROUPS, true) .putBoolean(MIGRATED_IGNORED_GROUPS, true)
.apply() .apply()
return this
} }
companion object { companion object {
@ -484,14 +461,7 @@ abstract class ComickFun(
private const val FIRST_COVER_DEFAULT = true private const val FIRST_COVER_DEFAULT = true
private const val SCORE_POSITION_PREF = "ScorePosition" private const val SCORE_POSITION_PREF = "ScorePosition"
private const val SCORE_POSITION_DEFAULT = "top" private const val SCORE_POSITION_DEFAULT = "top"
private const val limit = 20 private const val LIMIT = 20
val dateFormat by lazy { private const val CHAPTERS_LIMIT = 99999
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
val markdownLinksRegex = "\\[([^]]+)]\\(([^)]+)\\)".toRegex()
val markdownItalicBoldRegex = "\\*+\\s*([^*]*)\\s*\\*+".toRegex()
val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
} }
} }

View File

@ -10,7 +10,7 @@ val legacyLanguageMappings = mapOf(
"zh" to "zh-Hans", // Simplified Chinese "zh" to "zh-Hans", // Simplified Chinese
).withDefault { it } // country code matches language code ).withDefault { it } // country code matches language code
class ComickFunFactory : SourceFactory { class ComickFactory : SourceFactory {
private val idMap = listOf( private val idMap = listOf(
"all" to 982606170401027267, "all" to 982606170401027267,
"en" to 2971557565147974499, "en" to 2971557565147974499,
@ -55,7 +55,7 @@ class ComickFunFactory : SourceFactory {
"da" to 7137437402245830147, "da" to 7137437402245830147,
).toMap() ).toMap()
override fun createSources(): List<Source> = idMap.keys.map { override fun createSources(): List<Source> = idMap.keys.map {
object : ComickFun(legacyLanguageMappings.getValue(it), it) { object : Comick(legacyLanguageMappings.getValue(it), it) {
override val id: Long = idMap[it]!! override val id: Long = idMap[it]!!
} }
} }

View File

@ -7,7 +7,7 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import kotlin.system.exitProcess import kotlin.system.exitProcess
class ComickFunUrlActivity : Activity() { class ComickUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
@ -15,7 +15,7 @@ class ComickFunUrlActivity : Activity() {
val slug = pathSegments[1] val slug = pathSegments[1]
val mainIntent = Intent().apply { val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH" action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${ComickFun.SLUG_SEARCH_PREFIX}$slug") putExtra("query", "${Comick.SLUG_SEARCH_PREFIX}$slug")
putExtra("filter", packageName) putExtra("filter", packageName)
} }

View File

@ -8,9 +8,9 @@ import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
@Serializable @Serializable
data class SearchManga( class SearchManga(
val hid: String, private val hid: String,
val title: String, private val title: String,
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(), @SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
@SerialName("cover_url") val cover: String? = null, @SerialName("cover_url") val cover: String? = null,
) { ) {
@ -23,12 +23,12 @@ data class SearchManga(
} }
@Serializable @Serializable
data class Manga( class Manga(
val comic: Comic, val comic: Comic,
val artists: List<Name> = emptyList(), private val artists: List<Name> = emptyList(),
val authors: List<Name> = emptyList(), private val authors: List<Name> = emptyList(),
val genres: List<Name> = emptyList(), private val genres: List<Name> = emptyList(),
val demographic: String? = null, private val demographic: String? = null,
) { ) {
fun toSManga( fun toSManga(
includeMuTags: Boolean = false, includeMuTags: Boolean = false,
@ -90,10 +90,10 @@ data class Manga(
} }
@Serializable @Serializable
data class Comic( class Comic(
val hid: String, val hid: String,
val title: String, val title: String,
val country: String? = null, private val country: String? = null,
val slug: String? = null, val slug: String? = null,
@SerialName("md_titles") val altTitles: List<Title> = emptyList(), @SerialName("md_titles") val altTitles: List<Title> = emptyList(),
val desc: String? = null, val desc: String? = null,
@ -125,55 +125,54 @@ data class Comic(
} }
@Serializable @Serializable
data class MdGenres( class MdGenres(
@SerialName("md_genres") val name: Name? = null, @SerialName("md_genres") val name: Name? = null,
) )
@Serializable @Serializable
data class MuComicCategories( class MuComicCategories(
@SerialName("mu_comic_categories") val categories: List<MuCategories?> = emptyList(), @SerialName("mu_comic_categories") val categories: List<MuCategories?> = emptyList(),
) )
@Serializable @Serializable
data class MuCategories( class MuCategories(
@SerialName("mu_categories") val category: Title? = null, @SerialName("mu_categories") val category: Title? = null,
) )
@Serializable @Serializable
data class Covers( class Covers(
val md_covers: List<MDcovers> = emptyList(), @SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
) )
@Serializable @Serializable
data class MDcovers( class MDcovers(
val b2key: String?, val b2key: String?,
val vol: String? = null, val vol: String? = null,
) )
@Serializable @Serializable
data class Title( class Title(
val title: String?, val title: String?,
) )
@Serializable @Serializable
data class Name( class Name(
val name: String, val name: String,
) )
@Serializable @Serializable
data class ChapterList( class ChapterList(
val chapters: MutableList<Chapter>, val chapters: List<Chapter>,
val total: Int,
) )
@Serializable @Serializable
data class Chapter( class Chapter(
val hid: String, private val hid: String,
val lang: String = "", private val lang: String = "",
val title: String = "", private val title: String = "",
@SerialName("created_at") val createdAt: String = "", @SerialName("created_at") val createdAt: String = "",
val chap: String = "", private val chap: String = "",
val vol: String = "", private val vol: String = "",
@SerialName("group_name") val groups: List<String> = emptyList(), @SerialName("group_name") val groups: List<String> = emptyList(),
) { ) {
fun toSChapter(mangaUrl: String) = SChapter.create().apply { fun toSChapter(mangaUrl: String) = SChapter.create().apply {
@ -185,16 +184,16 @@ data class Chapter(
} }
@Serializable @Serializable
data class PageList( class PageList(
val chapter: ChapterPageData, val chapter: ChapterPageData,
) )
@Serializable @Serializable
data class ChapterPageData( class ChapterPageData(
val images: List<Page>, val images: List<Page>,
) )
@Serializable @Serializable
data class Page( class Page(
val url: String? = null, val url: String? = null,
) )

View File

@ -1,13 +1,19 @@
package eu.kanade.tachiyomi.extension.all.comickfun package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.dateFormat
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.markdownItalicBoldRegex
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.markdownItalicRegex
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.markdownLinksRegex
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.Interceptor
import okhttp3.Response
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
private val markdownLinksRegex = "\\[([^]]+)]\\(([^)]+)\\)".toRegex()
private val markdownItalicBoldRegex = "\\*+\\s*([^*]*)\\s*\\*+".toRegex()
private val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
internal fun String.beautifyDescription(): String { internal fun String.beautifyDescription(): String {
return Parser.unescapeEntities(this, false) return Parser.unescapeEntities(this, false)
@ -42,25 +48,6 @@ internal fun parseCover(thumbnailUrl: String?, mdCovers: List<MDcovers>): String
return thumbnailUrl?.replaceAfterLast("/", "$b2key#$vol") return thumbnailUrl?.replaceAfterLast("/", "$b2key#$vol")
} }
internal fun thumbnailIntercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val frag = request.url.fragment
if (frag.isNullOrEmpty()) return chain.proceed(request)
val response = chain.proceed(request)
if (!response.isSuccessful && response.code == 404) {
response.close()
val url = request.url.toString()
.replaceAfterLast("/", frag)
return chain.proceed(
request.newBuilder()
.url(url)
.build(),
)
}
return response
}
internal fun beautifyChapterName(vol: String, chap: String, title: String): String { internal fun beautifyChapterName(vol: String, chap: String, title: String): String {
return buildString { return buildString {
if (vol.isNotEmpty()) { if (vol.isNotEmpty()) {