Remove Comick (#10571)
This commit is contained in:
parent
955b86567b
commit
23543e9fb8
@ -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>
|
@ -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)
|
@ -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)
|
@ -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 |
@ -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
|
||||
}
|
||||
}
|
@ -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]!!
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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"),
|
||||
)
|
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user