parent
fbb06aadb9
commit
522b8c6293
|
@ -35,7 +35,7 @@ jobs:
|
|||
},
|
||||
{
|
||||
"type": "both",
|
||||
"regex": ".*(mangago|mangafox|hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|heroscan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|read\\s*comic\\s*online|coco\\s*manhua|hitomi\\.la).*",
|
||||
"regex": ".*(mangago|mangafox|hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|heroscan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|read\\s*comic\\s*online|coco\\s*manhua|hitomi\\.la|copymanga).*",
|
||||
"ignoreCase": true,
|
||||
"message": "{match} will not be added back as it is too difficult to maintain. Read #3475 for more information"
|
||||
},
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -1,26 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'CopyManga'
|
||||
pkgNameSuffix = 'zh.copymanga'
|
||||
extClass = '.CopyManga'
|
||||
extVersionCode = 31
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.luhuiguo:chinese-utils:1.0'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
android {
|
||||
packagingOptions {
|
||||
exclude '/pinyin.txt'
|
||||
exclude '/polyphone.txt'
|
||||
exclude '/trad.txt'
|
||||
exclude '/traditional.txt'
|
||||
exclude '/unknown.txt'
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 42 KiB |
|
@ -1,356 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.copymanga
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.extension.zh.copymanga.MangaDto.Companion.parseChapterGroups
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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 kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.Single
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class CopyManga : HttpSource(), ConfigurableSource {
|
||||
override val name = "拷贝漫画"
|
||||
override val lang = "zh"
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
|
||||
private var domain = DOMAINS[preferences.getString(DOMAIN_PREF, "0")!!.toInt().coerceIn(0, DOMAINS.size - 1)]
|
||||
override val baseUrl = WWW_PREFIX + domain
|
||||
private var apiUrl = API_PREFIX + domain // www. 也可以
|
||||
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
.addInterceptor(NonblockingRateLimitInterceptor(2, 4)) // 2 requests per 4 seconds
|
||||
.build()
|
||||
|
||||
private fun Headers.Builder.setUserAgent(userAgent: String) = set("User-Agent", userAgent)
|
||||
private fun Headers.Builder.setRegion(useOverseasCdn: Boolean) = set("region", if (useOverseasCdn) "0" else "1")
|
||||
private fun Headers.Builder.setReferer() = set("Referer", WWW_PREFIX + domain)
|
||||
private fun Headers.Builder.setVersion(version: String) = set("version", version)
|
||||
|
||||
override fun headersBuilder() = Headers.Builder()
|
||||
.setUserAgent(preferences.getString(USER_AGENT_PREF, DEFAULT_USER_AGENT)!!)
|
||||
.setRegion(preferences.getBoolean(OVERSEAS_CDN_PREF, false))
|
||||
.setReferer()
|
||||
.add("platform", "1")
|
||||
.setVersion(preferences.getString(VERSION_PREF, DEFAULT_VERSION)!!)
|
||||
|
||||
private var apiHeaders = headersBuilder().build()
|
||||
|
||||
private var useWebp = preferences.getBoolean(WEBP_PREF, true)
|
||||
|
||||
init {
|
||||
MangaDto.convertToSc = preferences.getBoolean(SC_TITLE_PREF, false)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val offset = PAGE_SIZE * (page - 1)
|
||||
return GET("$apiUrl/api/v3/recs?pos=3200102&limit=$PAGE_SIZE&offset=$offset", apiHeaders)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val page: ListDto<MangaWrapperDto> = response.parseAs()
|
||||
val hasNextPage = page.offset + page.limit < page.total
|
||||
return MangasPage(page.list.map { it.toSManga() }, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val offset = PAGE_SIZE * (page - 1)
|
||||
return GET("$apiUrl/api/v3/update/newest?limit=$PAGE_SIZE&offset=$offset", apiHeaders)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val offset = PAGE_SIZE * (page - 1)
|
||||
val builder = apiUrl.toHttpUrl().newBuilder()
|
||||
.addQueryParameter("limit", "$PAGE_SIZE")
|
||||
.addQueryParameter("offset", "$offset")
|
||||
if (query.isNotBlank()) {
|
||||
builder.addPathSegments("api/v3/search/comic")
|
||||
.addQueryParameter("q", query)
|
||||
filters.filterIsInstance<SearchFilter>().firstOrNull()?.addQuery(builder)
|
||||
} else {
|
||||
builder.addPathSegments("api/v3/comics")
|
||||
filters.filterIsInstance<CopyMangaFilter>().forEach {
|
||||
if (it !is SearchFilter) it.addQuery(builder)
|
||||
}
|
||||
}
|
||||
return Request.Builder().url(builder.build()).headers(apiHeaders).build()
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val page: ListDto<MangaDto> = response.parseAs()
|
||||
val hasNextPage = page.offset + page.limit < page.total
|
||||
return MangasPage(page.list.map { it.toSManga() }, hasNextPage)
|
||||
}
|
||||
|
||||
// 让 WebView 打开网页而不是 API
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(WWW_PREFIX + domain + manga.url, apiHeaders)
|
||||
|
||||
private fun realMangaDetailsRequest(manga: SManga) =
|
||||
GET("$apiUrl/api/v3/comic2/${manga.url.removePrefix(MangaDto.URL_PREFIX)}", apiHeaders)
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||
client.newCall(realMangaDetailsRequest(manga)).asObservableSuccess().map { mangaDetailsParse(it) }
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga =
|
||||
response.parseAs<MangaWrapperDto>().toSMangaDetails()
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Single.create<List<SChapter>> {
|
||||
val result = ArrayList<SChapter>()
|
||||
val groups = manga.description?.parseChapterGroups() ?: run {
|
||||
val response = client.newCall(realMangaDetailsRequest(manga)).execute()
|
||||
response.parseAs<MangaWrapperDto>().groups!!.values
|
||||
}
|
||||
val mangaSlug = manga.url.removePrefix(MangaDto.URL_PREFIX)
|
||||
result.fetchChapterGroup(mangaSlug, "default", "")
|
||||
for (group in groups) {
|
||||
result.fetchChapterGroup(mangaSlug, group.path_word, group.name)
|
||||
}
|
||||
it.onSuccess(result)
|
||||
}.toObservable()
|
||||
|
||||
private fun ArrayList<SChapter>.fetchChapterGroup(manga: String, key: String, name: String) {
|
||||
val result = ArrayList<SChapter>(0)
|
||||
var offset = 0
|
||||
var hasNextPage = true
|
||||
while (hasNextPage) {
|
||||
val response = client.newCall(GET("$apiUrl/api/v3/comic/$manga/group/$key/chapters?limit=$CHAPTER_PAGE_SIZE&offset=$offset", apiHeaders)).execute()
|
||||
val chapters: ListDto<ChapterDto> = response.parseAs()
|
||||
result.ensureCapacity(chapters.total)
|
||||
chapters.list.mapTo(result) { it.toSChapter(name) }
|
||||
offset += CHAPTER_PAGE_SIZE
|
||||
hasNextPage = offset < chapters.total
|
||||
}
|
||||
addAll(result.asReversed())
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga) = throw UnsupportedOperationException("Not used.")
|
||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
// 新版 API 中间是 /chapter2/ 并且返回值需要排序
|
||||
override fun pageListRequest(chapter: SChapter) = GET("$apiUrl/api/v3${chapter.url}", apiHeaders)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result: ChapterPageListWrapperDto = response.parseAs()
|
||||
if (result.show_app) {
|
||||
throw Exception("访问受限,请尝试在插件设置中修改 User Agent")
|
||||
}
|
||||
return result.chapter.contents.mapIndexed { i, it ->
|
||||
Page(i, imageUrl = it.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imageUrl = page.imageUrl!!
|
||||
return if (useWebp && imageUrl.endsWith(".jpg")) {
|
||||
GET(imageUrl.removeSuffix(".jpg") + ".webp")
|
||||
} else {
|
||||
GET(imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
if (header("Content-Type") != "application/json") {
|
||||
throw Exception("访问受限,请尝试在插件设置中修改 User Agent")
|
||||
} else if (code != 200) {
|
||||
throw Exception(json.decodeFromStream<ResultMessageDto>(body!!.byteStream()).message)
|
||||
}
|
||||
json.decodeFromStream<ResultDto<T>>(body!!.byteStream()).results
|
||||
}
|
||||
|
||||
private var genres: Array<Param> = emptyArray()
|
||||
private var isFetchingGenres = false
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val genreFilter = if (genres.isEmpty()) {
|
||||
fetchGenres()
|
||||
Filter.Header("点击“重置”尝试刷新题材分类")
|
||||
} else {
|
||||
GenreFilter(genres)
|
||||
}
|
||||
return FilterList(
|
||||
SearchFilter(),
|
||||
Filter.Separator(),
|
||||
Filter.Header("分类(搜索文本时无效)"),
|
||||
genreFilter,
|
||||
RegionFilter(),
|
||||
StatusFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun fetchGenres() {
|
||||
if (genres.isNotEmpty() || isFetchingGenres) return
|
||||
isFetchingGenres = true
|
||||
thread {
|
||||
try {
|
||||
val response = client.newCall(GET("$apiUrl/api/v3/theme/comic/count?limit=500", apiHeaders)).execute()
|
||||
val list = response.parseAs<ListDto<KeywordDto>>().list
|
||||
val result = ArrayList<Param>(list.size + 1).apply { add(Param("全部", "")) }
|
||||
genres = list.mapTo(result) { it.toParam() }.toTypedArray()
|
||||
} catch (e: Exception) {
|
||||
Log.e("CopyManga", "failed to fetch genres", e)
|
||||
} finally {
|
||||
isFetchingGenres = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fetchVersionState = 0 // 0 = not yet or failed, 1 = fetching, 2 = fetched
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = DOMAIN_PREF
|
||||
title = "网址域名"
|
||||
summary = "连接不稳定时可以尝试切换"
|
||||
entries = DOMAINS
|
||||
entryValues = DOMAIN_INDICES
|
||||
setDefaultValue("0")
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
domain = DOMAINS[(newValue as String).toInt()]
|
||||
apiUrl = API_PREFIX + domain
|
||||
apiHeaders = apiHeaders.newBuilder().setReferer().build()
|
||||
true
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = USER_AGENT_PREF
|
||||
title = "User Agent (UA)"
|
||||
summary = "可以使用 Windows/macOS/iOS 上浏览器的 UA,不要使用安卓浏览器和 Windows Chrome 103(“在 WebView 中打开”需要重启应用刷新)"
|
||||
setDefaultValue(DEFAULT_USER_AGENT)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
apiHeaders = apiHeaders.newBuilder().setUserAgent(newValue as String).build()
|
||||
true
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = UA_CHECKER
|
||||
title = "获取浏览器 UA 的链接"
|
||||
summary = "点击后可以在弹出的对话框中复制链接"
|
||||
setDefaultValue(UA_CHECKER)
|
||||
setOnPreferenceChangeListener { _, _ -> false }
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
title = "更新网页版本号"
|
||||
summary = "点击尝试更新网页版本号,当前为:${preferences.getString(VERSION_PREF, DEFAULT_VERSION)}"
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
if (fetchVersionState == 1) {
|
||||
Toast.makeText(screen.context, "已经在尝试更新,请勿反复点击", Toast.LENGTH_SHORT).show()
|
||||
return@setOnPreferenceChangeListener false
|
||||
} else if (fetchVersionState == 2) {
|
||||
Toast.makeText(screen.context, "版本号已经成功更新,返回重进刷新", Toast.LENGTH_SHORT).show()
|
||||
return@setOnPreferenceChangeListener false
|
||||
}
|
||||
Toast.makeText(screen.context, "开始尝试更新网页版本号", Toast.LENGTH_SHORT).show()
|
||||
fetchVersionState = 1
|
||||
thread {
|
||||
try {
|
||||
val headers = apiHeaders.newBuilder().setUserAgent(System.getProperty("http.agent")!!).build()
|
||||
val html = client.newCall(GET("https://www.copymanga.org/h5", headers)).execute().body!!.string()
|
||||
val jsRegex = Regex("""https\S+?index\.\w+?\.js""")
|
||||
val jsUrl = jsRegex.find(html)!!.value
|
||||
val js = client.newCall(GET(jsUrl, headers)).execute().body!!.string()
|
||||
val versionRegex = Regex("""VERSION:"([\d.]+?)"""", RegexOption.IGNORE_CASE)
|
||||
val version = versionRegex.find(js)!!.groupValues[1]
|
||||
preferences.edit().putString(VERSION_PREF, version).apply()
|
||||
apiHeaders = apiHeaders.newBuilder().setVersion(version).build()
|
||||
fetchVersionState = 2
|
||||
} catch (e: Throwable) {
|
||||
fetchVersionState = 0
|
||||
Log.e("CopyManga", "failed to fetch version", e)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = OVERSEAS_CDN_PREF
|
||||
title = "使用“港台及海外线路”"
|
||||
summary = "连接不稳定时可以尝试切换,关闭时使用“大陆用户线路”,已阅读章节需要清空缓存才能生效"
|
||||
setDefaultValue(false)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
apiHeaders = apiHeaders.newBuilder().setRegion(newValue as Boolean).build()
|
||||
true
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = WEBP_PREF
|
||||
title = "使用 WebP 图片格式"
|
||||
summary = "默认开启,可以节省网站流量"
|
||||
setDefaultValue(true)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
useWebp = newValue as Boolean
|
||||
true
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = SC_TITLE_PREF
|
||||
title = "将作品标题转换为简体中文"
|
||||
summary = "修改后,已添加漫画需要迁移才能更新标题"
|
||||
setDefaultValue(false)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
MangaDto.convertToSc = newValue as Boolean
|
||||
true
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DOMAIN_PREF = "domain"
|
||||
private const val OVERSEAS_CDN_PREF = "changeCDN"
|
||||
private const val SC_TITLE_PREF = "showSCTitle"
|
||||
private const val WEBP_PREF = "webp"
|
||||
private const val USER_AGENT_PREF = "userAgent"
|
||||
private const val VERSION_PREF = "version"
|
||||
// private const val CHROME_VERSION_PREF = "chromeVersion" // default value was "103"
|
||||
|
||||
private const val WWW_PREFIX = "https://www."
|
||||
private const val API_PREFIX = "https://api."
|
||||
private val DOMAINS = arrayOf("copymanga.org", "copymanga.info", "copymanga.net")
|
||||
private val DOMAIN_INDICES = arrayOf("0", "1", "2")
|
||||
private const val DEFAULT_USER_AGENT = ""
|
||||
private const val DEFAULT_VERSION = "2022.06.29"
|
||||
private const val UA_CHECKER = "https://tool.lu/useragent"
|
||||
|
||||
private const val PAGE_SIZE = 20
|
||||
private const val CHAPTER_PAGE_SIZE = 500
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.copymanga
|
||||
|
||||
import com.luhuiguo.chinese.ChineseUtils
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
val name: String,
|
||||
val path_word: String,
|
||||
val author: List<KeywordDto>,
|
||||
val cover: String,
|
||||
val region: ValueDto? = null,
|
||||
val status: ValueDto? = null,
|
||||
val theme: List<KeywordDto>? = null,
|
||||
val brief: String? = null,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = URL_PREFIX + path_word
|
||||
title = if (convertToSc) ChineseUtils.toSimplified(name) else name
|
||||
author = this@MangaDto.author.joinToString { it.name }
|
||||
thumbnail_url = cover.removeSuffix(".328x422.jpg")
|
||||
}
|
||||
|
||||
fun toSMangaDetails(groups: ChapterGroups) = toSManga().apply {
|
||||
description = brief + groups.toDescription()
|
||||
genre = buildList(theme!!.size + 1) {
|
||||
add(region!!.display)
|
||||
theme.mapTo(this) { it.name }
|
||||
}.joinToString { ChineseUtils.toSimplified(it) }
|
||||
status = when (this@MangaDto.status!!.value) {
|
||||
0 -> SManga.ONGOING
|
||||
1 -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
initialized = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal var convertToSc = false
|
||||
|
||||
const val URL_PREFIX = "/comic/"
|
||||
|
||||
private const val CHAPTER_GROUP_DELIMITER = ","
|
||||
private const val CHAPTER_GROUP_PREFIX = "\n\n【其他版本:"
|
||||
private const val CHAPTER_GROUP_POSTFIX = "】"
|
||||
private const val NO_CHAPTER_GROUP = "无"
|
||||
|
||||
private fun ChapterGroups.toDescription(): String {
|
||||
if (size <= 1) return CHAPTER_GROUP_PREFIX + NO_CHAPTER_GROUP + CHAPTER_GROUP_POSTFIX
|
||||
val groups = ArrayList<KeywordDto>(size - 1)
|
||||
for ((key, group) in this) {
|
||||
if (key != "default") groups.add(group)
|
||||
}
|
||||
return groups.joinToString(CHAPTER_GROUP_DELIMITER, CHAPTER_GROUP_PREFIX, CHAPTER_GROUP_POSTFIX) {
|
||||
it.name + '#' + it.path_word
|
||||
}
|
||||
}
|
||||
|
||||
fun String.parseChapterGroups(): List<KeywordDto>? {
|
||||
val index = lastIndexOf(CHAPTER_GROUP_PREFIX)
|
||||
if (index < 0) return null
|
||||
val groups = substring(index + CHAPTER_GROUP_PREFIX.length, length - CHAPTER_GROUP_POSTFIX.length)
|
||||
if (groups == NO_CHAPTER_GROUP) return emptyList()
|
||||
return groups.split(CHAPTER_GROUP_DELIMITER).map {
|
||||
val delimiterIndex = it.indexOf('#')
|
||||
KeywordDto(it.substring(0, delimiterIndex), it.substring(delimiterIndex + 1, it.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterDto(
|
||||
val uuid: String,
|
||||
val name: String,
|
||||
val comic_path_word: String,
|
||||
val datetime_created: String,
|
||||
) {
|
||||
fun toSChapter(group: String) = SChapter.create().apply {
|
||||
url = "/comic/$comic_path_word/chapter/$uuid"
|
||||
name = if (group.isEmpty()) this@ChapterDto.name else group + ':' + this@ChapterDto.name
|
||||
date_upload = dateFormat.parse(datetime_created)?.time ?: 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class KeywordDto(val name: String, val path_word: String) {
|
||||
fun toParam() = Param(ChineseUtils.toSimplified(name), path_word)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ValueDto(val value: Int, val display: String)
|
||||
|
||||
@Serializable
|
||||
class MangaWrapperDto(val comic: MangaDto, val groups: ChapterGroups? = null) {
|
||||
fun toSManga() = comic.toSManga()
|
||||
fun toSMangaDetails() = comic.toSMangaDetails(groups!!)
|
||||
}
|
||||
|
||||
typealias ChapterGroups = LinkedHashMap<String, KeywordDto>
|
||||
|
||||
@Serializable
|
||||
class ChapterPageListDto(val contents: List<UrlDto>)
|
||||
|
||||
@Serializable
|
||||
class UrlDto(val url: String)
|
||||
|
||||
@Serializable
|
||||
class ChapterPageListWrapperDto(val chapter: ChapterPageListDto, val show_app: Boolean)
|
||||
|
||||
@Serializable
|
||||
class ListDto<T>(
|
||||
val total: Int,
|
||||
val limit: Int,
|
||||
val offset: Int,
|
||||
val list: List<T>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ResultDto<T>(val results: T)
|
||||
|
||||
@Serializable
|
||||
class ResultMessageDto(val code: Int, val message: String)
|
|
@ -1,53 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.copymanga
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class Param(val name: String, val value: String)
|
||||
|
||||
open class CopyMangaFilter(name: String, private val key: String, private val params: Array<Param>) :
|
||||
Filter.Select<String>(name, params.map { it.name }.toTypedArray()) {
|
||||
fun addQuery(builder: HttpUrl.Builder) {
|
||||
val param = params[state].value
|
||||
if (param.isNotEmpty())
|
||||
builder.addQueryParameter(key, param)
|
||||
}
|
||||
}
|
||||
|
||||
class SearchFilter : CopyMangaFilter("文本搜索范围", "q_type", SEARCH_FILTER_VALUES)
|
||||
|
||||
private val SEARCH_FILTER_VALUES = arrayOf(
|
||||
Param("全部", ""),
|
||||
Param("名称", "name"),
|
||||
Param("作者", "author"),
|
||||
Param("汉化组", "local"),
|
||||
)
|
||||
|
||||
class GenreFilter(genres: Array<Param>) : CopyMangaFilter("题材", "theme", genres)
|
||||
|
||||
class RegionFilter : CopyMangaFilter("地区", "region", REGION_VALUES)
|
||||
|
||||
private val REGION_VALUES = arrayOf(
|
||||
Param("全部", ""),
|
||||
Param("日本", "0"),
|
||||
Param("韩国", "1"),
|
||||
Param("欧美", "2"),
|
||||
)
|
||||
|
||||
class StatusFilter : CopyMangaFilter("状态", "status", STATUS_VALUES)
|
||||
|
||||
private val STATUS_VALUES = arrayOf(
|
||||
Param("全部", ""),
|
||||
Param("连载中", "0"),
|
||||
Param("已完结", "1"),
|
||||
Param("短篇", "2"),
|
||||
)
|
||||
|
||||
class SortFilter : CopyMangaFilter("排序", "ordering", SORT_VALUES)
|
||||
|
||||
private val SORT_VALUES = arrayOf(
|
||||
Param("热门", "-popular"),
|
||||
Param("热门(逆序)", "popular"),
|
||||
Param("更新时间", "-datetime_updated"),
|
||||
Param("更新时间(逆序)", "datetime_updated"),
|
||||
)
|
|
@ -1,58 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.zh.copymanga
|
||||
|
||||
import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// See https://github.com/tachiyomiorg/tachiyomi/pull/7389
|
||||
internal class NonblockingRateLimitInterceptor(
|
||||
private val permits: Int,
|
||||
period: Long = 1,
|
||||
unit: TimeUnit = TimeUnit.SECONDS,
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayList<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
// Ignore canceled calls, otherwise they would jam the queue
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException()
|
||||
}
|
||||
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
0
|
||||
} else {
|
||||
val oldestReq = requestQueue[0]
|
||||
val newestReq = requestQueue[permits - 1]
|
||||
|
||||
if (newestReq - oldestReq > rateLimitMillis) {
|
||||
0
|
||||
} else {
|
||||
oldestReq + rateLimitMillis - now // Remaining time
|
||||
}
|
||||
}
|
||||
|
||||
// Final check
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException()
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
if (waitTime > 0) {
|
||||
requestQueue.add(now + waitTime)
|
||||
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
|
||||
} else {
|
||||
requestQueue.add(now)
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue