Comick: some improvements (#16611)

* Comick: some improvements

* comick: thumbnail preference

* comick: alt titles

* use update date
This commit is contained in:
AwkwardPeak7 2023-05-30 19:50:45 +05:00 committed by GitHub
parent 5b5aa0af94
commit b7c39c7a67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 182 additions and 85 deletions

View File

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

View File

@ -1,7 +1,12 @@
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.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
@ -16,11 +21,15 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import java.text.ParseException 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
abstract class ComickFun(override val lang: String, private val comickFunLang: String) : HttpSource() { abstract class ComickFun(
override val lang: String,
private val comickFunLang: String,
) : HttpSource(), ConfigurableSource {
override val name = "Comick" override val name = "Comick"
@ -28,8 +37,6 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
private val apiUrl = "https://api.comick.fun" private val apiUrl = "https://api.comick.fun"
private val cdnUrl = "https://meo3.comick.pictures"
override val supportsLatest = true override val supportsLatest = true
private val json = Json { private val json = Json {
@ -44,7 +51,14 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}") add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
} }
override val client: OkHttpClient = network.client.newBuilder().rateLimit(4, 1).build() override val client: OkHttpClient = network.client.newBuilder()
.addNetworkInterceptor(::thumbnailIntercept)
.rateLimit(4, 1)
.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 {
@ -79,7 +93,8 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
} }
val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX) val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX)
return fetchMangaDetails(SManga.create().apply { this.url = "/comic/$slugOrHid#" }).map { val manga = SManga.create().apply { this.url = "/comic/$slugOrHid#" }
return fetchMangaDetails(manga).map {
MangasPage(listOf(it), false) MangasPage(listOf(it), false)
} }
} }
@ -170,18 +185,9 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val isQueryPresent = response.request.url.queryParameterNames.contains("q") val isQueryPresent = response.request.url.queryParameterNames.contains("q")
val result = json.decodeFromString<List<Manga>>(response.body.string()) val result = response.parseAs<List<SearchManga>>()
return MangasPage( return MangasPage(
result.map { data -> result.map { it.toSManga(useScaledCover) },
SManga.create().apply {
// appennding # at end as part of migration from slug to hid
url = "/comic/${data.hid}#"
title = data.title
thumbnail_url = runCatching {
"$cdnUrl/${data.md_covers.first().b2key}"
}.getOrNull()
}
},
/* /*
api always returns `limit` amount of results api always returns `limit` amount of results
for text search and page>=2 is always empty for text search and page>=2 is always empty
@ -204,20 +210,8 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val mangaData = json.decodeFromString<MangaDetails>(response.body.string()) val mangaData = response.parseAs<Manga>()
return SManga.create().apply { return mangaData.toSManga(useScaledCover)
// appennding # at end as part of migration from slug to hid
url = "/comic/${mangaData.comic.hid}#"
title = mangaData.comic.title
artist = mangaData.artists.joinToString { it.name.trim() }
author = mangaData.authors.joinToString { it.name.trim() }
description = beautifyDescription(mangaData.comic.desc)
genre = mangaData.genres.joinToString { it.name.trim() }
status = parseStatus(mangaData.comic.status)
thumbnail_url = runCatching {
"$cdnUrl/${mangaData.comic.md_covers.first().b2key}"
}.getOrNull()
}
} }
override fun getMangaUrl(manga: SManga): String { override fun getMangaUrl(manga: SManga): String {
@ -247,7 +241,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val chapterListResponse = json.decodeFromString<ChapterList>(response.body.string()) val chapterListResponse = response.parseAs<ChapterList>()
val mangaUrl = response.request.url.toString() val mangaUrl = response.request.url.toString()
.substringBefore("/chapters") .substringBefore("/chapters")
@ -259,7 +253,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
while (chapterListResponse.total > resultSize) { while (chapterListResponse.total > resultSize) {
val newRequest = paginatedChapterListRequest(mangaUrl, page) val newRequest = paginatedChapterListRequest(mangaUrl, page)
val newResponse = client.newCall(newRequest).execute() val newResponse = client.newCall(newRequest).execute()
val newChapterListResponse = json.decodeFromString<ChapterList>(newResponse.body.string()) val newChapterListResponse = newResponse.parseAs<ChapterList>()
chapterListResponse.chapters += newChapterListResponse.chapters chapterListResponse.chapters += newChapterListResponse.chapters
@ -267,24 +261,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
page += 1 page += 1
} }
return chapterListResponse.chapters.map { chapter -> return chapterListResponse.chapters.map { it.toSChapter(mangaUrl) }
SChapter.create().apply {
url = "$mangaUrl/${chapter.hid}-chapter-${chapter.chap}-$comickFunLang"
name = beautifyChapterName(chapter.vol, chapter.chap, chapter.title)
date_upload = chapter.created_at.let {
try {
dateFormat.parse(it)?.time ?: 0L
} catch (e: ParseException) {
0L
}
}
scanlator = chapter.group_name.joinToString().takeUnless { it.isBlank() } ?: "Unknown"
}
}
}
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
} }
override fun getChapterUrl(chapter: SChapter): String { override fun getChapterUrl(chapter: SChapter): String {
@ -298,14 +275,14 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val result = json.decodeFromString<PageList>(response.body.string()) val result = response.parseAs<PageList>()
return result.chapter.images.mapIndexedNotNull { index, data -> return result.chapter.images.mapIndexedNotNull { index, data ->
if (data.url == null) null else Page(index = index, imageUrl = data.url) if (data.url == null) null else Page(index = index, imageUrl = data.url)
} }
} }
companion object { private inline fun <reified T> Response.parseAs(): T {
const val SLUG_SEARCH_PREFIX = "id:" return json.decodeFromString(body.string())
} }
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String {
@ -318,4 +295,30 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
getFilters(), 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 {
const val SLUG_SEARCH_PREFIX = "id:"
val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
}
val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex()
val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex()
val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
private const val coverQualityPref = "pref_cover_quality"
}
} }

View File

@ -1,12 +1,64 @@
package eu.kanade.tachiyomi.extension.all.comickfun package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Manga( data class SearchManga(
val hid: String, val hid: String,
val title: String, val title: String,
val md_covers: List<MDcovers>, val md_covers: List<MDcovers>,
val cover_url: String? = null,
) {
fun toSManga(useScaledCover: Boolean) = SManga.create().apply {
// appennding # at end as part of migration from slug to hid
url = "/comic/$hid#"
title = this@SearchManga.title
thumbnail_url = parseCover(cover_url, md_covers, useScaledCover)
}
}
@Serializable
data class Manga(
val comic: Comic,
val artists: List<Artist> = emptyList(),
val authors: List<Author> = emptyList(),
val genres: List<Genre> = emptyList(),
) {
fun toSManga(useScaledCover: Boolean) = SManga.create().apply {
// appennding # at end as part of migration from slug to hid
url = "/comic/${comic.hid}#"
title = comic.title
description = comic.desc.beautifyDescription()
if (comic.altTitles.isNotEmpty()) {
description += comic.altTitles.joinToString(
separator = "\n",
prefix = "\n\nAlternative Titles:\n",
) {
it.title.toString()
}
}
status = comic.status.parseStatus(comic.translation_completed)
thumbnail_url = parseCover(comic.cover_url, comic.md_covers, useScaledCover)
artist = artists.joinToString { it.name.trim() }
author = authors.joinToString { it.name.trim() }
genre = genres.joinToString { it.name.trim() }
}
}
@Serializable
data class Comic(
val hid: String,
val title: String,
@SerialName("md_titles") val altTitles: List<MDtitles>,
val desc: String = "N/A",
val status: Int = 0,
val translation_completed: Boolean = true,
val md_covers: List<MDcovers>,
val cover_url: String? = null,
) )
@Serializable @Serializable
@ -15,20 +67,8 @@ data class MDcovers(
) )
@Serializable @Serializable
data class MangaDetails( data class MDtitles(
val comic: Comic, val title: String?,
val artists: Array<Artist>,
val authors: Array<Author>,
val genres: Array<Genre>,
)
@Serializable
data class Comic(
val hid: String,
val title: String,
val desc: String = "N/A",
val status: Int,
val md_covers: List<MDcovers>,
) )
@Serializable @Serializable
@ -58,12 +98,20 @@ data class ChapterList(
@Serializable @Serializable
data class Chapter( data class Chapter(
val hid: String, val hid: String,
val lang: String,
val title: String = "", val title: String = "",
val created_at: String = "", val updated_at: String = "",
val chap: String = "", val chap: String = "",
val vol: String = "", val vol: String = "",
val group_name: Array<String> = arrayOf(""), val group_name: List<String> = emptyList(),
) ) {
fun toSChapter(mangaUrl: String) = SChapter.create().apply {
url = "$mangaUrl/$hid-chapter-$chap-$lang"
name = beautifyChapterName(vol, chap, title)
date_upload = updated_at.parseDate()
scanlator = group_name.joinToString().takeUnless { it.isBlank() } ?: "Unknown"
}
}
@Serializable @Serializable
data class PageList( data class PageList(
@ -72,7 +120,7 @@ data class PageList(
@Serializable @Serializable
data class ChapterPageData( data class ChapterPageData(
val images: Array<Page>, val images: List<Page>,
) )
@Serializable @Serializable

View File

@ -1,27 +1,68 @@
package eu.kanade.tachiyomi.extension.all.comickfun package eu.kanade.tachiyomi.extension.all.comickfun
import android.os.Build import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.dateFormat
import android.text.Html 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 org.jsoup.Jsoup import okhttp3.Interceptor
import okhttp3.Response
import org.jsoup.parser.Parser
internal fun beautifyDescription(description: String): String { internal fun String.beautifyDescription(): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return Parser.unescapeEntities(this, false)
return Html.fromHtml(description, Html.FROM_HTML_MODE_LEGACY).toString() .replace(markdownLinksRegex, "")
} .replace(markdownItalicBoldRegex, "")
return Jsoup.parse(description).text() .replace(markdownItalicRegex, "")
.trim()
} }
internal fun parseStatus(status: Int): Int { internal fun Int.parseStatus(translationComplete: Boolean): Int {
return when (status) { return when (this) {
1 -> SManga.ONGOING 1 -> SManga.ONGOING
2 -> SManga.COMPLETED 2 -> {
if (translationComplete) {
SManga.COMPLETED
} else {
SManga.PUBLISHING_FINISHED
}
}
3 -> SManga.CANCELLED 3 -> SManga.CANCELLED
4 -> SManga.ON_HIATUS 4 -> SManga.ON_HIATUS
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
} }
internal fun parseCover(thumbnailUrl: String?, mdCovers: List<MDcovers>, useScaled: Boolean): String? {
val b2key = runCatching { mdCovers.first().b2key }
.getOrNull() ?: ""
return if (useScaled) {
"$thumbnailUrl#$b2key"
} else {
thumbnailUrl?.replaceAfterLast("/", b2key)
}
}
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()) {
@ -35,3 +76,8 @@ internal fun beautifyChapterName(vol: String, chap: String, title: String): Stri
} }
} }
} }
internal fun String.parseDate(): Long {
return runCatching { dateFormat.parse(this)?.time }
.getOrNull() ?: 0L
}