Remove Comick (#10571)

This commit is contained in:
AwkwardPeak7 2025-09-19 11:52:42 +05:00 committed by Draff
parent 955b86567b
commit 23543e9fb8
Signed by: Draff
GPG Key ID: E8A89F3211677653
15 changed files with 0 additions and 1506 deletions

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.comickfun.ComickUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="comick.io" />
<data android:host="comick.cc" />
<data android:host="comick.ink" />
<data android:host="comick.app" />
<data android:host="comick.fun" />
<data android:pathPattern="/comic/.*/..*" />
<data android:pathPattern="/comic/..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,33 +0,0 @@
ignored_groups_title=Ignored Groups
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
preferred_groups_title=Preferred Groups
preferred_groups_summary=Chapters from these groups will have priority over others (Even if lower score).\nOne group name per line (case-insensitive)
ignored_tags_title=Ignored Tags
ignored_tags_summary=Manga with these tags won't show up when browsing.\nOne tag per line (case-insensitive)
show_alternative_titles_title=Show Alternative Titles
show_alternative_titles_on=Adds alternative titles to the description
show_alternative_titles_off=Does not show alternative titles to the description
include_tags_title=Include Tags
include_tags_on=More specific, but might contain spoilers!
include_tags_off=Only the broader genres
group_tags_title=Group Tags (fork must support grouping)
group_tags_on=Will prefix tags with their type
group_tags_off=List all tags together
update_cover_title=Update Covers
update_cover_on=Keep cover updated
update_cover_off=Prefer first cover
local_title_title=Translated Title
local_title_on=if available
local_title_off=Use the default title from the site
score_position_title=Score Position in the Description
score_position_top=Top
score_position_middle=Middle
score_position_bottom=Bottom
score_position_none=Hide Score
cover_quality_title=Cover Quality
cover_quality_original=Original
cover_quality_compressed=Compressed
cover_quality_web_default=Small (Web Default)
chapter_score_filtering_title=Automatically de-duplicate chapters
chapter_score_filtering_on=For each chapter, only displays the scanlator with the highest score
chapter_score_filtering_off=Does not filterout any chapters based on score (any other scanlator filtering will still apply)

View File

@ -1,33 +0,0 @@
ignored_groups_title=Grupos Ignorados
ignored_groups_summary=Capítulos desses grupos não serão mostrados.\nUm nome de grupo por linha (não diferencia maiúsculas de minúsculas)
preferred_groups_title=Grupos Preferidos
preferred_groups_summary=Capítulos desses grupos terão prioridade sobre os outros (Mesmo que com nota mais baixa).\nUm nome de grupo por linha (não diferencia maiúsculas de minúsculas)
ignored_tags_title=Tags Ignoradas
ignored_tags_summary=Mangás com essas tags não aparecerão ao navegar.\nUma tag por linha (não diferencia maiúsculas de minúsculas)
show_alternative_titles_title=Mostrar Títulos Alternativos
show_alternative_titles_on=Adiciona títulos alternativos à descrição
show_alternative_titles_off=Não mostra títulos alternativos na descrição
include_tags_title=Incluir Tags
include_tags_on=Mais específicas, mas podem conter spoilers!
include_tags_off=Apenas os gêneros básicos
group_tags_title=Agrupar Tags (necessário fork compatível)
group_tags_on=Irá prefixar tags com o respectivo tipo
group_tags_off=Listar todas as tags juntas
update_cover_title=Atualizar Capas
update_cover_on=Manter capa atualizada
update_cover_off=Preferir a primeira capa
local_title_title=Título Traduzido
local_title_on=se disponível
local_title_off=Usar o título padrão do site
score_position_title=Posição da Nota na Descrição
score_position_top=Topo
score_position_middle=Meio
score_position_bottom=Final
score_position_none=Esconder Nota
cover_quality_title=Qualidade da Capa
cover_quality_original=Original
cover_quality_compressed=Comprimida
cover_quality_web_default=Pequena (Padrão Web)
chapter_score_filtering_title=Desduplicar capítulos automaticamente
chapter_score_filtering_on=Para cada capítulo, exibe apenas o scanlator com a maior nota
chapter_score_filtering_off=Não filtra nenhum capítulo com base na nota (outros filtros de scanlator ainda se aplicarão)

View File

@ -1,12 +0,0 @@
ext {
extName = 'Comick'
extClass = '.ComickFactory'
extVersionCode = 62
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:i18n"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,773 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Builder
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit
import kotlin.math.min
abstract class Comick(
override val lang: String,
private val comickLang: String,
) : ConfigurableSource, HttpSource() {
override val name = "Comick"
override val baseUrl = "https://comick.io"
private val apiUrl = "https://api.comick.fun"
override val supportsLatest = true
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
coerceInputValues = true
explicitNulls = true
}
private lateinit var searchResponse: List<SearchManga>
private val intl by lazy {
Intl(
language = lang,
baseLanguage = "en",
availableLanguages = setOf("en", "pt-BR"),
classLoader = this::class.java.classLoader!!,
)
}
private val preferences by getPreferencesLazy { newLineIgnoredGroups() }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
key = IGNORED_GROUPS_PREF
title = intl["ignored_groups_title"]
summary = intl["ignored_groups_summary"]
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putString(IGNORED_GROUPS_PREF, newValue.toString())
.commit()
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = PREFERRED_GROUPS_PREF
title = intl["preferred_groups_title"]
summary = intl["preferred_groups_summary"]
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putString(PREFERRED_GROUPS_PREF, newValue.toString())
.commit()
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = IGNORED_TAGS_PREF
title = intl["ignored_tags_title"]
summary = intl["ignored_tags_summary"]
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ALTERNATIVE_TITLES_PREF
title = intl["show_alternative_titles_title"]
summaryOn = intl["show_alternative_titles_on"]
summaryOff = intl["show_alternative_titles_off"]
setDefaultValue(SHOW_ALTERNATIVE_TITLES_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(SHOW_ALTERNATIVE_TITLES_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = INCLUDE_MU_TAGS_PREF
title = intl["include_tags_title"]
summaryOn = intl["include_tags_on"]
summaryOff = intl["include_tags_off"]
setDefaultValue(INCLUDE_MU_TAGS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(INCLUDE_MU_TAGS_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = GROUP_TAGS_PREF
title = intl["group_tags_title"]
summaryOn = intl["group_tags_on"]
summaryOff = intl["group_tags_off"]
setDefaultValue(GROUP_TAGS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(GROUP_TAGS_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = FIRST_COVER_PREF
title = intl["update_cover_title"]
summaryOff = intl["update_cover_off"]
summaryOn = intl["update_cover_on"]
setDefaultValue(FIRST_COVER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(FIRST_COVER_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = COVER_QUALITY_PREF
title = intl["cover_quality_title"]
entries = arrayOf(
intl["cover_quality_original"],
intl["cover_quality_compressed"],
intl["cover_quality_web_default"],
)
entryValues = arrayOf(
"Original",
"Compressed",
COVER_QUALITY_DEFAULT,
)
setDefaultValue(COVER_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putString(COVER_QUALITY_PREF, newValue as String)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = LOCAL_TITLE_PREF
title = intl["local_title_title"]
summaryOff = intl["local_title_off"]
summaryOn = intl["local_title_on"]
setDefaultValue(LOCAL_TITLE_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(LOCAL_TITLE_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = SCORE_POSITION_PREF
title = intl["score_position_title"]
summary = "%s"
entries = arrayOf(
intl["score_position_top"],
intl["score_position_middle"],
intl["score_position_bottom"],
intl["score_position_none"],
)
entryValues = arrayOf(SCORE_POSITION_DEFAULT, "middle", "bottom", "none")
setDefaultValue(SCORE_POSITION_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit()
.putString(SCORE_POSITION_PREF, entry)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = CHAPTER_SCORE_FILTERING_PREF
title = intl["chapter_score_filtering_title"]
summaryOff = intl["chapter_score_filtering_off"]
summaryOn = intl["chapter_score_filtering_on"]
setDefaultValue(CHAPTER_SCORE_FILTERING_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(CHAPTER_SCORE_FILTERING_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
}
private val SharedPreferences.ignoredGroups: Set<String>
get() = getString(IGNORED_GROUPS_PREF, "")
?.lowercase()
?.split("\n")
?.map(String::trim)
?.filter(String::isNotEmpty)
?.sorted()
.orEmpty()
.toSet()
private val SharedPreferences.preferredGroups: Set<String>
get() = getString(PREFERRED_GROUPS_PREF, "")
?.lowercase()
?.split("\n")
?.map(String::trim)
?.filter(String::isNotEmpty)
.orEmpty()
.toSet()
private val SharedPreferences.ignoredTags: String
get() = getString(IGNORED_TAGS_PREF, "")
?.split("\n")
?.map(String::trim)
?.filter(String::isNotEmpty)
.orEmpty()
.joinToString(",")
private val SharedPreferences.showAlternativeTitles: Boolean
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
private val SharedPreferences.includeMuTags: Boolean
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
private val SharedPreferences.groupTags: Boolean
get() = getBoolean(GROUP_TAGS_PREF, GROUP_TAGS_DEFAULT)
private val SharedPreferences.updateCover: Boolean
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
private val coverQuality: CoverQuality
get() = CoverQuality.valueOf(
preferences.getString(COVER_QUALITY_PREF, COVER_QUALITY_DEFAULT) ?: COVER_QUALITY_DEFAULT,
)
private val SharedPreferences.localTitle: String
get() = if (getBoolean(
LOCAL_TITLE_PREF,
LOCAL_TITLE_DEFAULT,
)
) {
comickLang.lowercase()
} else {
"all"
}
private val SharedPreferences.scorePosition: String
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
private val SharedPreferences.chapterScoreFiltering: Boolean
get() = getBoolean(CHAPTER_SCORE_FILTERING_PREF, CHAPTER_SCORE_FILTERING_DEFAULT)
override fun headersBuilder() = Headers.Builder().apply {
add("Referer", "$baseUrl/")
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
}
override val client = network.cloudflareClient.newBuilder()
.addNetworkInterceptor(::errorInterceptor)
.addInterceptor(::imageInterceptor)
.rateLimit(5, 6, TimeUnit.SECONDS) // == 50req each (60sec / 1min)
.build()
private val imageClient = network.cloudflareClient.newBuilder()
.rateLimit(7, 4, TimeUnit.SECONDS) // == 1.75req/1sec == 14req/8sec == 105req/60sec
.build()
private val smallThumbnailClient = network.cloudflareClient.newBuilder()
.rateLimit(14, 1, TimeUnit.SECONDS)
.build()
private fun imageInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url.toString()
return if ("comick.pictures" in url && "-s." in url) {
smallThumbnailClient.newCall(request).execute()
} else if ("comick.pictures" in url) {
imageClient.newCall(request).execute()
} else {
chain.proceed(request)
}
}
private fun errorInterceptor(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (
response.isSuccessful ||
"application/json" !in response.header("Content-Type").orEmpty()
) {
return response
}
val error = try {
response.parseAs<Error>()
} catch (_: Exception) {
null
}
error?.run {
throw Exception("$name error $statusCode: $message")
} ?: throw Exception("HTTP error ${response.code}")
}
/** Popular Manga **/
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortFilter("follow"),
),
)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = response.parseAs<List<SearchManga>>()
return MangasPage(
result.map(SearchManga::toSManga),
hasNextPage = result.size >= LIMIT,
)
}
/** Latest Manga **/
override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortFilter("uploaded"),
),
)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
/** Manga Search **/
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return if (query.startsWith(SLUG_SEARCH_PREFIX)) {
// url deep link
val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX)
val manga = SManga.create().apply { this.url = "/comic/$slugOrHid#" }
fetchMangaDetails(manga).map {
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)
}
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
tags.split(",").filter(String::isNotEmpty).forEach {
builder.addQueryParameter(
parameterName,
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
.replace("'-", "-and-039-").replace("'", "-and-039-"),
)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
filters.forEach { it ->
when (it) {
is CompletedFilter -> {
if (it.state) {
addQueryParameter("completed", "true")
}
}
is GenreFilter -> {
it.state.filter { it.isIncluded() }.forEach {
addQueryParameter("genres", it.value)
}
it.state.filter { it.isExcluded() }.forEach {
addQueryParameter("excludes", it.value)
}
}
is DemographicFilter -> {
it.state.filter { it.state }.forEach {
addQueryParameter("demographic", it.value)
}
}
is TypeFilter -> {
it.state.filter { it.state }.forEach {
addQueryParameter("country", it.value)
}
}
is SortFilter -> {
addQueryParameter("sort", it.getValue())
}
is StatusFilter -> {
if (it.state > 0) {
addQueryParameter("status", it.getValue())
}
}
is ContentRatingFilter -> {
if (it.state > 0) {
addQueryParameter("content_rating", it.getValue())
}
}
is CreatedAtFilter -> {
if (it.state > 0) {
addQueryParameter("time", it.getValue())
}
}
is MinimumFilter -> {
if (it.state.isNotEmpty()) {
addQueryParameter("minimum", it.state)
}
}
is FromYearFilter -> {
if (it.state.isNotEmpty()) {
addQueryParameter("from", it.state)
}
}
is ToYearFilter -> {
if (it.state.isNotEmpty()) {
addQueryParameter("to", it.state)
}
}
is TagFilter -> {
if (it.state.isNotEmpty()) {
addTagQueryParameters(this, it.state, "tags")
}
}
is ExcludedTagFilter -> {
if (it.state.isNotEmpty()) {
addTagQueryParameters(this, it.state, "excluded-tags")
}
}
else -> {}
}
}
addTagQueryParameters(this, preferences.ignoredTags, "excluded-tags")
addQueryParameter("tachiyomi", "true")
addQueryParameter("limit", "$LIMIT")
addQueryParameter("page", "$page")
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
/** Manga Details **/
override fun mangaDetailsRequest(manga: SManga): Request {
// Migration from slug based urls to hid based ones
if (!manga.url.endsWith("#")) {
throw Exception("Migrate from Comick to Comick")
}
val mangaUrl = manga.url.removeSuffix("#")
return GET("$apiUrl$mangaUrl?tachiyomi=true", headers)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response, manga).apply { initialized = true }
}
}
override fun mangaDetailsParse(response: Response): SManga =
mangaDetailsParse(response, SManga.create())
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
val mangaData = response.parseAs<Manga>()
if (!preferences.updateCover && manga.thumbnail_url != mangaData.comic.cover) {
val coversUrl =
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
val covers = client.newCall(GET(coversUrl)).execute()
.parseAs<Covers>().mdCovers.reversed()
val firstVol = covers.filter { it.vol == "1" }.ifEmpty { covers }
val originalCovers = firstVol
.filter { mangaData.comic.isoLang.orEmpty().startsWith(it.locale.orEmpty()) }
val localCovers = firstVol
.filter { comickLang.startsWith(it.locale.orEmpty()) }
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition,
showAlternativeTitles = preferences.showAlternativeTitles,
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
groupTags = preferences.groupTags,
titleLang = preferences.localTitle,
coverQuality = coverQuality,
)
}
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition,
showAlternativeTitles = preferences.showAlternativeTitles,
groupTags = preferences.groupTags,
titleLang = preferences.localTitle,
coverQuality = coverQuality,
)
}
override fun getMangaUrl(manga: SManga): String {
return "$baseUrl${manga.url.removeSuffix("#")}"
}
/** Manga Chapter List **/
override fun chapterListRequest(manga: SManga): Request {
// Migration from slug based urls to hid based ones
if (!manga.url.endsWith("#")) {
throw Exception("Migrate from Comick to Comick")
}
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()
return GET(url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val chapterListResponse = response.parseAs<ChapterList>()
val preferredGroups = preferences.preferredGroups
val ignoredGroupsLowercase = preferences.ignoredGroups.map { it.lowercase() }
val mangaUrl = response.request.url.toString()
.substringBefore("/chapters")
.substringAfter(apiUrl)
val currentTimestamp = System.currentTimeMillis()
// First, apply the ignored groups filter to remove chapters from blocked groups
val filteredChapters = chapterListResponse.chapters
.filter {
val publishTime = try {
publishedDateFormat.parse(it.publishedAt)!!.time
} catch (_: ParseException) {
0L
}
val publishedChapter = publishTime <= currentTimestamp
val noGroupBlock = it.groups.map { g -> g.lowercase() }
.intersect(ignoredGroupsLowercase)
.isEmpty()
publishedChapter && noGroupBlock
}
// Now apply the primary filtering logic based on user preferences
val finalChapters = if (preferredGroups.isEmpty()) {
// If preferredGroups is empty, fall back to the existing score filter
filteredChapters.filterOnScore(preferences.chapterScoreFiltering)
} else {
// If preferredGroups is not empty, use the list to grab chapters from those groups in order of preference
val chaptersByNumber = filteredChapters.groupBy { it.chap }
val preferredFilteredChapters = mutableListOf<Chapter>()
// Iterate through each chapter number's group of chapters
chaptersByNumber.forEach { (_, chaptersForNumber) ->
// Find the chapter from the most preferred group
val preferredChapter = preferredGroups.firstNotNullOfOrNull { preferredGroup ->
chaptersForNumber.find { chapter ->
chapter.groups.any { group ->
group.lowercase() == preferredGroup.lowercase()
}
}
}
if (preferredChapter != null) {
preferredFilteredChapters.add(preferredChapter)
} else {
// If no preferred group chapter was found, fall back to the score filter
val fallbackChapter = chaptersForNumber.filterOnScore(preferences.chapterScoreFiltering)
preferredFilteredChapters.addAll(fallbackChapter)
}
}
preferredFilteredChapters
}
// Finally, map the filtered chapters to the SChapter model
return finalChapters.map { it.toSChapter(mangaUrl) }
}
private fun List<Chapter>.filterOnScore(shouldFilter: Boolean): Collection<Chapter> {
if (shouldFilter) {
return groupBy { it.chap }
.map { (_, chapters) -> chapters.maxBy { it.score } }
} else {
return this
}
}
private val publishedDateFormat =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl${chapter.url}"
}
/** Chapter Pages **/
override fun pageListRequest(chapter: SChapter): Request {
val chapterHid = chapter.url.substringAfterLast("/").substringBefore("-")
return GET("$apiUrl/chapter/$chapterHid?tachiyomi=true", headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<PageList>()
val images = result.chapter.images.ifEmpty {
// cache busting
val url = response.request.url.newBuilder()
.addQueryParameter("_", System.currentTimeMillis().toString())
.build()
client.newCall(GET(url, headers)).execute()
.parseAs<PageList>().chapter.images
}
return images.mapIndexedNotNull { index, data ->
if (data.url == null) null else Page(index = index, imageUrl = data.url)
}
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
override fun getFilterList() = getFilters()
private fun SharedPreferences.newLineIgnoredGroups() {
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
edit()
.putString(
IGNORED_GROUPS_PREF,
ignoredGroups
.split(",")
.map(String::trim)
.filter(String::isNotEmpty)
.joinToString("\n"),
)
.putBoolean(MIGRATED_IGNORED_GROUPS, true)
.apply()
}
companion object {
const val SLUG_SEARCH_PREFIX = "id:"
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
private const val PREFERRED_GROUPS_PREF = "PreferredGroups"
private const val IGNORED_TAGS_PREF = "IgnoredTags"
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
const val INCLUDE_MU_TAGS_DEFAULT = false
private const val GROUP_TAGS_PREF = "GroupTags"
const val GROUP_TAGS_DEFAULT = false
private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups"
private const val FIRST_COVER_PREF = "DefaultCover"
private const val FIRST_COVER_DEFAULT = true
private const val COVER_QUALITY_PREF = "CoverQuality"
const val COVER_QUALITY_DEFAULT = "WebDefault"
private const val SCORE_POSITION_PREF = "ScorePosition"
const val SCORE_POSITION_DEFAULT = "top"
private const val LOCAL_TITLE_PREF = "LocalTitle"
private const val LOCAL_TITLE_DEFAULT = false
private const val CHAPTER_SCORE_FILTERING_PREF = "ScoreAutoFiltering"
private const val CHAPTER_SCORE_FILTERING_DEFAULT = false
private const val LIMIT = 20
private const val CHAPTERS_LIMIT = 99999
}
}

View File

@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
// A legacy mapping of language codes to ensure that source IDs don't change
val legacyLanguageMappings = mapOf(
"pt-br" to "pt-BR", // Brazilian Portuguese
"zh-hk" to "zh-Hant", // Traditional Chinese,
"zh" to "zh-Hans", // Simplified Chinese
).withDefault { it } // country code matches language code
class ComickFactory : SourceFactory {
private val idMap = listOf(
"all" to 982606170401027267,
"en" to 2971557565147974499,
"pt-br" to 8729626158695297897,
"ru" to 5846182885417171581,
"fr" to 9126078936214680667,
"es-419" to 3182432228546767958,
"pl" to 7005108854993254607,
"tr" to 7186425300860782365,
"it" to 8807318985460553537,
"es" to 9052019484488287695,
"id" to 5506707690027487154,
"hu" to 7838940669485160901,
"vi" to 9191587139933034493,
"zh-hk" to 3140511316190656180,
"ar" to 8266599095155001097,
"de" to 7552236568334706863,
"zh" to 1071494508319622063,
"ca" to 2159382907508433047,
"bg" to 8981320463367739957,
"th" to 4246541831082737053,
"fa" to 3146252372540608964,
"uk" to 3505068018066717349,
"mn" to 2147260678391898600,
"ro" to 6676949771764486043,
"he" to 5354540502202034685,
"ms" to 4731643595200952045,
"tl" to 8549617092958820123,
"ja" to 8288710818308434509,
"hi" to 5176570178081213805,
"my" to 9199495862098963317,
"ko" to 3493720175703105662,
"cs" to 2651978322082769022,
"pt" to 4153491877797434408,
"nl" to 6104206360977276112,
"sv" to 979314012722687145,
"bn" to 3598159956413889411,
"no" to 5932005504194733317,
"lt" to 1792260331167396074,
"el" to 6190162673651111756,
"sr" to 571668187470919545,
"da" to 7137437402245830147,
).toMap()
override fun createSources(): List<Source> = idMap.keys.map {
object : Comick(legacyLanguageMappings.getValue(it), it) {
override val id: Long = idMap[it]!!
}
}
}

View File

@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class ComickUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val slug = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Comick.SLUG_SEARCH_PREFIX}$slug")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("ComickFunUrlActivity", e.toString())
}
} else {
Log.e("ComickFunUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,239 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.GROUP_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.INCLUDE_MU_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SCORE_POSITION_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SHOW_ALTERNATIVE_TITLES_DEFAULT
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.math.BigDecimal
import java.math.RoundingMode
@Serializable
class SearchManga(
private val hid: String,
private val title: String,
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
@SerialName("cover_url") val cover: String? = null,
) {
fun toSManga() = SManga.create().apply {
// appending # at end as part of migration from slug to hid
url = "/comic/$hid#"
title = this@SearchManga.title
thumbnail_url = parseCover(cover, mdCovers)
}
}
@Serializable
class Manga(
val comic: Comic,
private val artists: List<Name> = emptyList(),
private val authors: List<Name> = emptyList(),
private val genres: List<Genre> = emptyList(),
private val demographic: String? = null,
) {
fun toSManga(
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
scorePosition: String = SCORE_POSITION_DEFAULT,
showAlternativeTitles: Boolean = SHOW_ALTERNATIVE_TITLES_DEFAULT,
covers: List<MDcovers>? = null,
groupTags: Boolean = GROUP_TAGS_DEFAULT,
titleLang: String,
coverQuality: CoverQuality = CoverQuality.Compressed,
): SManga {
val entryTitle = comic.altTitles.firstOrNull {
titleLang != "all" && !it.lang.isNullOrBlank() && titleLang.startsWith(it.lang)
}?.title ?: comic.title
val titles = listOf(Title(title = comic.title)) + comic.altTitles
return SManga.create().apply {
// appennding # at end as part of migration from slug to hid
url = "/comic/${comic.hid}#"
title = entryTitle
description = buildString {
if (scorePosition == "top") append(comic.fancyScore)
val desc = comic.desc?.beautifyDescription()
if (!desc.isNullOrEmpty()) {
if (this.isNotEmpty()) append("\n\n")
append(desc)
}
if (scorePosition == "middle") {
if (this.isNotEmpty()) append("\n\n")
append(comic.fancyScore)
}
if (showAlternativeTitles && comic.altTitles.isNotEmpty()) {
if (this.isNotEmpty()) append("\n\n")
append("Alternative Titles:\n")
append(
titles.distinctBy { it.title }.filter { it.title != entryTitle }
.mapNotNull { title ->
title.title?.let { "$it" }
}.joinToString("\n"),
)
}
if (scorePosition == "bottom") {
if (this.isNotEmpty()) append("\n\n")
append(comic.fancyScore)
}
}
status = comic.status.parseStatus(comic.translationComplete)
thumbnail_url = parseCover(
comic.cover,
covers ?: comic.mdCovers,
coverQuality,
)
artist = artists.joinToString { it.name.trim() }
author = authors.joinToString { it.name.trim() }
genre = buildList {
comic.origination?.let { add(Genre("Origination", it.name)) }
demographic?.let { add(Genre("Demographic", it)) }
addAll(
comic.mdGenres.mapNotNull { it.genre }.sortedBy { it.group }
.sortedBy { it.name },
)
addAll(genres.sortedBy { it.group }.sortedBy { it.name })
if (includeMuTags) {
addAll(
comic.muGenres.categories.mapNotNull { it?.category?.title }.sorted()
.map { Genre("Category", it) },
)
}
}
.distinctBy { it.name }
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() }
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
}
}
}
@Serializable
class Comic(
val hid: String,
val title: String,
private val country: String? = null,
val slug: String? = null,
@SerialName("md_titles") val altTitles: List<Title> = emptyList(),
val desc: String? = null,
val status: Int? = 0,
@SerialName("translation_completed") val translationComplete: Boolean? = true,
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
@SerialName("cover_url") val cover: String? = null,
@SerialName("md_comic_md_genres") val mdGenres: List<MdGenres>,
@SerialName("mu_comics") val muGenres: MuComicCategories = MuComicCategories(emptyList()),
@SerialName("bayesian_rating") val score: String? = null,
@SerialName("iso639_1") val isoLang: String? = null,
) {
val origination = when (country) {
"jp" -> Name("Manga")
"kr" -> Name("Manhwa")
"cn" -> Name("Manhua")
else -> null
}
val fancyScore: String = if (score.isNullOrEmpty()) {
""
} else {
val stars = score.toBigDecimal().div(BigDecimal(2))
.setScale(0, RoundingMode.HALF_UP).toInt()
buildString {
append("".repeat(stars))
if (stars < 5) append("".repeat(5 - stars))
append(" $score")
}
}
}
@Serializable
class MdGenres(
@SerialName("md_genres") val genre: Genre? = null,
)
@Serializable
class Genre(
val group: String? = null,
val name: String? = null,
)
@Serializable
class MuComicCategories(
@SerialName("mu_comic_categories") val categories: List<MuCategories?> = emptyList(),
)
@Serializable
class MuCategories(
@SerialName("mu_categories") val category: Title? = null,
)
@Serializable
class Covers(
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
)
@Serializable
class MDcovers(
val b2key: String?,
val vol: String? = null,
val locale: String? = null,
)
@Serializable
class Title(
val title: String?,
val lang: String? = null,
)
@Serializable
class Name(
val name: String,
)
@Serializable
class ChapterList(
val chapters: List<Chapter>,
)
@Serializable
class Chapter(
private val hid: String,
private val lang: String = "",
private val title: String = "",
@SerialName("created_at") private val createdAt: String = "",
@SerialName("publish_at") val publishedAt: String = "",
val chap: String = "",
private val vol: String = "",
@SerialName("group_name") val groups: List<String> = emptyList(),
@SerialName("up_count") private val upCount: Int,
@SerialName("down_count") private val downCount: Int,
) {
val score get() = upCount - downCount
fun toSChapter(mangaUrl: String) = SChapter.create().apply {
url = "$mangaUrl/$hid-chapter-$chap-$lang"
name = beautifyChapterName(vol, chap, title)
date_upload = createdAt.parseDate()
scanlator = groups.joinToString().takeUnless { it.isBlank() } ?: "Unknown"
}
}
@Serializable
class PageList(
val chapter: ChapterPageData,
)
@Serializable
class ChapterPageData(
val images: List<Page>,
)
@Serializable
class Page(
val url: String? = null,
)
@Serializable
class Error(
val statusCode: Int,
val message: String,
)

View File

@ -1,208 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
Filter.Header(name = "The filter is ignored when using text search."),
GenreFilter("Genre", getGenresList),
DemographicFilter("Demographic", getDemographicList),
TypeFilter("Type", getTypeList),
SortFilter(),
StatusFilter("Status", getStatusList),
ContentRatingFilter("Content Rating", getContentRatingList),
CompletedFilter("Completely Scanlated?"),
CreatedAtFilter("Created at", getCreatedAtList),
MinimumFilter("Minimum Chapters"),
Filter.Header("From Year, ex: 2010"),
FromYearFilter("From"),
Filter.Header("To Year, ex: 2021"),
ToYearFilter("To"),
Filter.Header("Separate tags with commas"),
TagFilter("Tags"),
ExcludedTagFilter("Excluded Tags"),
)
}
/** Filters **/
internal class GenreFilter(name: String, genreList: List<Pair<String, String>>) :
Filter.Group<TriFilter>(name, genreList.map { TriFilter(it.first, it.second) })
internal class TagFilter(name: String) : TextFilter(name)
internal class ExcludedTagFilter(name: String) : TextFilter(name)
internal class DemographicFilter(name: String, demographicList: List<Pair<String, String>>) :
Filter.Group<CheckBoxFilter>(name, demographicList.map { CheckBoxFilter(it.first, it.second) })
internal class TypeFilter(name: String, typeList: List<Pair<String, String>>) :
Filter.Group<CheckBoxFilter>(name, typeList.map { CheckBoxFilter(it.first, it.second) })
internal class CompletedFilter(name: String) : CheckBoxFilter(name)
internal class CreatedAtFilter(name: String, createdAtList: List<Pair<String, String>>) :
SelectFilter(name, createdAtList)
internal class MinimumFilter(name: String) : TextFilter(name)
internal class FromYearFilter(name: String) : TextFilter(name)
internal class ToYearFilter(name: String) : TextFilter(name)
internal class SortFilter(defaultValue: String? = null, state: Int = 0) :
SelectFilter("Sort", getSortsList, state, defaultValue)
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
internal class ContentRatingFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
/** Generics **/
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
internal open class TextFilter(name: String) : Filter.Text(name)
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name)
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0, defaultValue: String? = null) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: state) {
fun getValue() = vals[state].second
}
/** Filters Data **/
private val getGenresList: List<Pair<String, String>> = listOf(
Pair("4-Koma", "4-koma"),
Pair("Action", "action"),
Pair("Adaptation", "adaptation"),
Pair("Adult", "adult"),
Pair("Adventure", "adventure"),
Pair("Aliens", "aliens"),
Pair("Animals", "animals"),
Pair("Anthology", "anthology"),
Pair("Award Winning", "award-winning"),
Pair("Comedy", "comedy"),
Pair("Cooking", "cooking"),
Pair("Crime", "crime"),
Pair("Crossdressing", "crossdressing"),
Pair("Delinquents", "delinquents"),
Pair("Demons", "demons"),
Pair("Doujinshi", "doujinshi"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Fan Colored", "fan-colored"),
Pair("Fantasy", "fantasy"),
Pair("Full Color", "full-color"),
Pair("Gender Bender", "gender-bender"),
Pair("Genderswap", "genderswap"),
Pair("Ghosts", "ghosts"),
Pair("Gore", "gore"),
Pair("Gyaru", "gyaru"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Incest", "incest"),
Pair("Isekai", "isekai"),
Pair("Loli", "loli"),
Pair("Long Strip", "long-strip"),
Pair("Mafia", "mafia"),
Pair("Magic", "magic"),
Pair("Magical Girls", "magical-girls"),
Pair("Martial Arts", "martial-arts"),
Pair("Mature", "mature"),
Pair("Mecha", "mecha"),
Pair("Medical", "medical"),
Pair("Military", "military"),
Pair("Monster Girls", "monster-girls"),
Pair("Monsters", "monsters"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Ninja", "ninja"),
Pair("Office Workers", "office-workers"),
Pair("Official Colored", "official-colored"),
Pair("Oneshot", "oneshot"),
Pair("Philosophical", "philosophical"),
Pair("Police", "police"),
Pair("Post-Apocalyptic", "post-apocalyptic"),
Pair("Psychological", "psychological"),
Pair("Reincarnation", "reincarnation"),
Pair("Reverse Harem", "reverse-harem"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("School Life", "school-life"),
Pair("Sci-Fi", "sci-fi"),
Pair("Sexual Violence", "sexual-violence"),
Pair("Shota", "shota"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounen Ai", "shounen-ai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Smut", "smut"),
Pair("Sports", "sports"),
Pair("Superhero", "superhero"),
Pair("Supernatural", "supernatural"),
Pair("Survival", "survival"),
Pair("Thriller", "thriller"),
Pair("Time Travel", "time-travel"),
Pair("Traditional Games", "traditional-games"),
Pair("Tragedy", "tragedy"),
Pair("User Created", "user-created"),
Pair("Vampires", "vampires"),
Pair("Video Games", "video-games"),
Pair("Villainess", "villainess"),
Pair("Virtual Reality", "virtual-reality"),
Pair("Web Comic", "web-comic"),
Pair("Wuxia", "wuxia"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Zombies", "zombies"),
)
private val getDemographicList: List<Pair<String, String>> = listOf(
Pair("Shounen", "1"),
Pair("Shoujo", "2"),
Pair("Seinen", "3"),
Pair("Josei", "4"),
Pair("None", "5"),
)
private val getTypeList: List<Pair<String, String>> = listOf(
Pair("Manga", "jp"),
Pair("Manhwa", "kr"),
Pair("Manhua", "cn"),
Pair("Others", "others"),
)
private val getCreatedAtList: List<Pair<String, String>> = listOf(
Pair("", ""),
Pair("3 days", "3"),
Pair("7 days", "7"),
Pair("30 days", "30"),
Pair("3 months", "90"),
Pair("6 months", "180"),
Pair("1 year", "365"),
)
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Most popular", "follow"),
Pair("Most follows", "user_follow_count"),
Pair("Most views", "view"),
Pair("High rating", "rating"),
Pair("Last updated", "uploaded"),
Pair("Newest", "created_at"),
)
private val getStatusList: List<Pair<String, String>> = listOf(
Pair("All", "0"),
Pair("Ongoing", "1"),
Pair("Completed", "2"),
Pair("Cancelled", "3"),
Pair("Hiatus", "4"),
)
private val getContentRatingList: List<Pair<String, String>> = listOf(
Pair("All", ""),
Pair("Safe", "safe"),
Pair("Suggestive", "suggestive"),
Pair("Erotica", "erotica"),
)

View File

@ -1,85 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.model.SManga
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:ssXXX", 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 {
return Parser.unescapeEntities(this, false)
.substringBefore("---")
.replace(markdownLinksRegex, "")
.replace(markdownItalicBoldRegex, "")
.replace(markdownItalicRegex, "")
.trim()
}
internal fun Int?.parseStatus(translationComplete: Boolean?): Int {
return when (this) {
1 -> SManga.ONGOING
2 -> {
if (translationComplete == true) {
SManga.COMPLETED
} else {
SManga.PUBLISHING_FINISHED
}
}
3 -> SManga.CANCELLED
4 -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
enum class CoverQuality {
Original, // HQ original
Compressed, // HQ but compressed
WebDefault, // what comick serves in browser, usually compressed + downscaled
}
internal fun parseCover(thumbnailUrl: String?, mdCovers: List<MDcovers>, coverQuality: CoverQuality = CoverQuality.WebDefault): String? {
fun addOrReplaceCoverQualitySuffix(url: String, qualitySuffix: String): String {
return url.substringBeforeLast('#').substringBeforeLast('.').replace(Regex("-(m|s)$"), "") +
"$qualitySuffix.jpg#${url.substringAfter('#', "")}"
}
val mdCover = mdCovers.firstOrNull()
val coverUrl = if (mdCover != null) {
thumbnailUrl?.replaceAfterLast("/", "${mdCover.b2key}#${mdCover.vol.orEmpty()}")
} else {
thumbnailUrl
} ?: return null
return when (coverQuality) {
CoverQuality.Original -> coverUrl
CoverQuality.Compressed -> addOrReplaceCoverQualitySuffix(coverUrl, "-m")
CoverQuality.WebDefault -> addOrReplaceCoverQualitySuffix(coverUrl, "-s")
}
}
internal fun beautifyChapterName(vol: String, chap: String, title: String): String {
return buildString {
if (vol.isNotEmpty()) {
if (chap.isEmpty()) append("Volume $vol") else append("Vol. $vol")
}
if (chap.isNotEmpty()) {
if (vol.isEmpty()) append("Chapter $chap") else append(", Ch. $chap")
}
if (title.isNotEmpty()) {
if (chap.isEmpty()) append(title) else append(": $title")
}
}
}
internal fun String.parseDate(): Long {
return runCatching { dateFormat.parse(this)?.time }
.getOrNull() ?: 0L
}