1
0
mirror of https://github.com/fumiama/copymanga.git synced 2026-06-27 06:10:29 +08:00
注意
> 由于大版本更新, 闪退问题可能增加.

>由于修复 bug, 更新可能比较频繁, 如无 API 代理需求可以暂缓更新.

新增
1. 更安全的 API 代理, 旧版代理将无法使用
2. 关于显示插件版本
3. 更改默认API (close #113)
修复
1. 无法搜索汉字漫画
2. 加载API组件无法显示进度
3. 可能的误触发网络增强
4. 不使用API密钥无法加载 (fix #117)
优化
1. 代码组织架构
This commit is contained in:
源文雨
2025-03-28 17:14:55 +09:00
parent d169c51153
commit c8cc5222b0
13 changed files with 158 additions and 102 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId 'top.fumiama.copymanga' applicationId 'top.fumiama.copymanga'
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 34 targetSdkVersion 34
versionCode 68 versionCode 69
versionName '2.4.1' versionName '2.4.2'
resourceConfigurations += ['zh', 'zh-rCN'] resourceConfigurations += ['zh', 'zh-rCN']
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -68,13 +68,13 @@ class Book(val path: String, private val getString: (Int) -> String, private val
isDownload = true isDownload = true
Config.apiProxy?.comancry(mBookApiUrl) { url -> Config.apiProxy?.comancry(mBookApiUrl) { url ->
DownloadTools.getHttpContent(url, null, mUserAgent) DownloadTools.getHttpContent(url, null, mUserAgent)
} }?:DownloadTools.getHttpContent(mBookApiUrl, null, mUserAgent)
} }
} else { } else {
isDownload = true isDownload = true
Config.apiProxy?.comancry(mBookApiUrl) { url -> Config.apiProxy?.comancry(mBookApiUrl) { url ->
DownloadTools.getHttpContent(url, null, mUserAgent) DownloadTools.getHttpContent(url, null, mUserAgent)
} }?:DownloadTools.getHttpContent(mBookApiUrl, null, mUserAgent)
}?:DownloadTools.getHttpContent(mBookApiUrl, null, mUserAgent) }?:DownloadTools.getHttpContent(mBookApiUrl, null, mUserAgent)
mBook = data.inputStream().use { mBook = data.inputStream().use {
Gson().fromJson(it.reader(), BookInfoStructure::class.java) Gson().fromJson(it.reader(), BookInfoStructure::class.java)

View File

@@ -25,11 +25,13 @@ class Shelf(private val getString: (Int) -> String) {
append("") append("")
append(Config.token.value) append(Config.token.value)
} }
val re = Config.apiProxy?.comancry(addApiUrl) { url -> val re = (Config.apiProxy?.comancry(addApiUrl) { url ->
DownloadTools.requestWithBody( DownloadTools.requestWithBody(
url, "POST", body.encodeToByteArray() url, "POST", body.encodeToByteArray()
) )
}?.decodeToString() ?: return@withContext "空回应" }?:DownloadTools.requestWithBody(
addApiUrl, "POST", body.encodeToByteArray()
))?.decodeToString() ?: return@withContext "空回应"
return@withContext try { return@withContext try {
Gson().fromJson(re, ReturnBase::class.java).message Gson().fromJson(re, ReturnBase::class.java).message
} catch (e: Exception) { } catch (e: Exception) {
@@ -50,11 +52,13 @@ class Shelf(private val getString: (Int) -> String) {
append("authorization=Token+") append("authorization=Token+")
append(Config.token.value) append(Config.token.value)
} }
val re = Config.apiProxy?.comancry(delApiUrl) { url -> val re = (Config.apiProxy?.comancry(delApiUrl) { url ->
DownloadTools.requestWithBody( DownloadTools.requestWithBody(
url, "DELETE", body.encodeToByteArray() url, "DELETE", body.encodeToByteArray()
) )
}?.decodeToString() ?: return@withContext "空回应" }?:DownloadTools.requestWithBody(
delApiUrl, "DELETE", body.encodeToByteArray()
))?.decodeToString() ?: return@withContext "空回应"
return@withContext try { return@withContext try {
Gson().fromJson(re, ReturnBase::class.java).message Gson().fromJson(re, ReturnBase::class.java).message
} catch (e: Exception) { } catch (e: Exception) {
@@ -64,9 +68,10 @@ class Shelf(private val getString: (Int) -> String) {
suspend fun query(pathWord: String): BookQueryStructure? = withContext(Dispatchers.IO) { suspend fun query(pathWord: String): BookQueryStructure? = withContext(Dispatchers.IO) {
try { try {
Config.apiProxy?.comancry(queryApiUrlTemplate.format(Config.myHostApiUrl.value, pathWord)) { url -> val queryUrl = queryApiUrlTemplate.format(Config.myHostApiUrl.value, pathWord)
(Config.apiProxy?.comancry(queryUrl) { url ->
DownloadTools.getHttpContent(url, Config.referer) DownloadTools.getHttpContent(url, Config.referer)
}?.let { }?:DownloadTools.getHttpContent(queryUrl, Config.referer)).let {
Gson().fromJson(it.decodeToString(), BookQueryStructure::class.java) Gson().fromJson(it.decodeToString(), BookQueryStructure::class.java)
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -48,12 +48,11 @@ class Member(private val getString: (Int) -> String) {
return@withContext l return@withContext l
} }
try { try {
val data = Config.apiProxy?.comancry( val u = getString(R.string.memberInfoApiUrl)
getString(R.string.memberInfoApiUrl) .format(Config.myHostApiUrl.value)
.format(Config.myHostApiUrl.value) val data = (Config.apiProxy?.comancry(u) {
) {
DownloadTools.getHttpContent(it) DownloadTools.getHttpContent(it)
}?.decodeToString() }?:DownloadTools.getHttpContent(u)).decodeToString()
try { try {
val l = Gson().fromJson(data, LoginInfoStructure::class.java) val l = Gson().fromJson(data, LoginInfoStructure::class.java)
if (l.code == 200) Config.avatar.value = l.results.avatar if (l.code == 200) Config.avatar.value = l.results.avatar
@@ -98,62 +97,68 @@ class Member(private val getString: (Int) -> String) {
} }
private suspend fun postLogin(username: String, pwd: String, salt: Int): ByteArray? = private suspend fun postLogin(username: String, pwd: String, salt: Int): ByteArray? =
Config.apiProxy?.comancry(getString(R.string.loginApiUrl).format(Config.myHostApiUrl.value)) { getString(R.string.loginApiUrl).format(Config.myHostApiUrl.value).let { u ->
DownloadTools.getApiConnection(it, "POST").let { c -> val use: suspend (String) -> ByteArray? = { it: String ->
c.doOutput = true DownloadTools.getApiConnection(it, "POST").let { c ->
c.setRequestProperty( c.doOutput = true
"content-type", c.setRequestProperty(
"application/x-www-form-urlencoded;charset=utf-8" "content-type",
) "application/x-www-form-urlencoded;charset=utf-8"
c.setRequestProperty("platform", "3") )
c.setRequestProperty("accept", "application/json") c.setRequestProperty("platform", "3")
val r = if (!Config.net_use_foreign.value) "1" else "0" c.setRequestProperty("accept", "application/json")
val pwdEncoded = val r = if (!Config.net_use_foreign.value) "1" else "0"
Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString() val pwdEncoded =
c.outputStream.write( Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString()
"username=${ c.outputStream.write(
URLEncoder.encode( "username=${
username, URLEncoder.encode(
Charset.defaultCharset().name() username,
) Charset.defaultCharset().name()
}&password=$pwdEncoded&salt=$salt&platform=3&authorization=Token+&version=${Config.app_ver.value}&source=copyApp&region=$r&webp=1".toByteArray() )
) }&password=$pwdEncoded&salt=$salt&platform=3&authorization=Token+&version=${Config.app_ver.value}&source=copyApp&region=$r&webp=1".toByteArray()
c.outputStream.close() )
val b = c.inputStream.readBytes() c.outputStream.close()
c.inputStream.close() val b = c.inputStream.readBytes()
b c.inputStream.close()
b
}
} }
Config.apiProxy?.comancry(u, use)?:use(u)
} }
private suspend fun postComandyLogin(username: String, pwd: String, salt: Int) = private suspend fun postComandyLogin(username: String, pwd: String, salt: Int) =
Config.apiProxy?.comancry(getString(R.string.loginApiUrl).format(Config.myHostApiUrl.value)) { getString(R.string.loginApiUrl).format(Config.myHostApiUrl.value).let { u ->
DownloadTools.getComandyApiConnection(it, "POST", null, Config.pc_ua).apply { val use: suspend (String) -> ByteArray? = { it: String ->
headers["content-type"] = "application/x-www-form-urlencoded;charset=utf-8" DownloadTools.getComandyApiConnection(it, "POST", null, Config.pc_ua).apply {
headers["platform"] = "3" headers["content-type"] = "application/x-www-form-urlencoded;charset=utf-8"
headers["accept"] = "application/json" headers["platform"] = "3"
val r = if (!Config.net_use_foreign.value) "1" else "0" headers["accept"] = "application/json"
val pwdEncoded = val r = if (!Config.net_use_foreign.value) "1" else "0"
Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString() val pwdEncoded =
data = "username=${ Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString()
URLEncoder.encode( data = "username=${
username, URLEncoder.encode(
Charset.defaultCharset().name() username,
) Charset.defaultCharset().name()
}&password=$pwdEncoded&salt=$salt&platform=3&authorization=Token+&version=${Config.app_ver.value}&source=copyApp&region=$r&webp=1" )
}.let { capsule -> }&password=$pwdEncoded&salt=$salt&platform=3&authorization=Token+&version=${Config.app_ver.value}&source=copyApp&region=$r&webp=1"
try { }.let { capsule ->
val para = Gson().toJson(capsule) try {
Comandy.instance.getInstance()?.request(para)?.let { result -> val para = Gson().toJson(capsule)
Gson().fromJson(result, ComandyCapsule::class.java)!!.let { Comandy.instance.getInstance()?.request(para)?.let { result ->
if (it.code != 200) null Gson().fromJson(result, ComandyCapsule::class.java)!!.let {
else Base64.decode(it.data, Base64.DEFAULT) if (it.code != 200) null
else Base64.decode(it.data, Base64.DEFAULT)
}
} }
} catch (e: Exception) {
e.printStackTrace()
null
} }
} catch (e: Exception) {
e.printStackTrace()
null
} }
} }
Config.apiProxy?.comancry(u, use)?:use(u)
} }
} }

View File

@@ -9,7 +9,7 @@ class Comancry: LazyLibrary<ComancryMethods>(
ComancryMethods::class.java, "libcomancry.so", "API代理", ComancryMethods::class.java, "libcomancry.so", "API代理",
Config.net_use_api_proxy, Config.comancry_version Config.net_use_api_proxy, Config.comancry_version
) { ) {
val enabled: Boolean private val enabled: Boolean
get() { get() {
if (isInInit.get()) { if (isInInit.get()) {
Log.d("MyComancry", "$name block enabled for isInInit") Log.d("MyComancry", "$name block enabled for isInInit")

View File

@@ -16,7 +16,7 @@ class Comandy: LazyLibrary<ComandyMethods>(
Log.d("MyComandy", "$name block enabled for isInInit") Log.d("MyComandy", "$name block enabled for isInInit")
return false return false
} }
if (mEnabled != true && DownloadTools.failTimes.get() >= 2) { if (mEnabled != true && DownloadTools.failTimes.get() >= 3) {
mEnabled = true mEnabled = true
return true return true
} }

View File

@@ -6,4 +6,6 @@ interface ComandyMethods : Library {
// fun add_dns(para: String?, is_ipv6: Int): String? // fun add_dns(para: String?, is_ipv6: Int): String?
fun request(para: String): String? fun request(para: String): String?
fun progress(para: String): Int
} }

View File

@@ -102,7 +102,9 @@ open class LazyLibrary<T: Library>(
if (remoteVersion > 0) version.value = remoteVersion if (remoteVersion > 0) version.value = remoteVersion
Log.d("MyLazyLibrary", "update success") Log.d("MyLazyLibrary", "update success")
isInInit.set(false) isInInit.set(false)
info.dismiss() withContext(Dispatchers.Main) {
info.dismiss()
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
if(f.exists()) f.delete() if(f.exists()) f.delete()

View File

@@ -85,7 +85,7 @@ class ComandyGlideModule: AppGlideModule() {
} }
override fun handles(model: GlideUrl): Boolean { override fun handles(model: GlideUrl): Boolean {
return Comandy.instance.enabled && runBlocking { Comandy.instance.getInstance() } != null && model.toURL().let { return Comandy.instance.enabled && model.toURL().let {
it.protocol == "https" && it.host != "copymanga.azurewebsites.net" it.protocol == "https" && it.host != "copymanga.azurewebsites.net"
} }
} }
@@ -103,7 +103,9 @@ class ComandyGlideModule: AppGlideModule() {
} }
override fun handles(model: String): Boolean { override fun handles(model: String): Boolean {
return Comandy.instance.enabled && runBlocking { Comandy.instance.getInstance() } != null && model.startsWith("https://") return Comandy.instance.enabled &&
model.startsWith("https://") &&
!model.startsWith("https://copymanga.azurewebsites.net")
} }
} }

View File

@@ -9,11 +9,11 @@ import kotlinx.coroutines.withContext
import top.fumiama.copymanga.MainActivity import top.fumiama.copymanga.MainActivity
import top.fumiama.copymanga.api.Config import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.json.ComandyCapsule import top.fumiama.copymanga.json.ComandyCapsule
import top.fumiama.copymanga.lib.Comancry
import top.fumiama.copymanga.lib.Comandy import top.fumiama.copymanga.lib.Comandy
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.lang.Thread.sleep
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.concurrent.Callable import java.util.concurrent.Callable
@@ -123,18 +123,43 @@ object DownloadTools {
getComandyApiConnection(u, "GET", refer, ua).let { capsule -> getComandyApiConnection(u, "GET", refer, ua).let { capsule ->
val para = Gson().toJson(capsule) val para = Gson().toJson(capsule)
//Log.d("MyDT", "comandy request: $para") //Log.d("MyDT", "comandy request: $para")
Comandy.instance.getInstance()?.request(para)?.let { result -> Comandy.instance.getInstance()?.let { ins ->
//Log.d("MyDT", "comandy reply: $result") var completed = false
Gson().fromJson(result, ComandyCapsule::class.java)!!.let { p?.let {
if (it.code != 200) throw IllegalArgumentException("HTTP${it.code} ${ Thread {
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() } Log.d("MyDT", "launch comandy get progress, completed: $completed for url $u")
}") var prev = 0
Base64.decode(it.data, Base64.DEFAULT) while (!completed) {
sleep(50)
val progress = ins.progress(para)
Log.d("MyDT", "comandy get progress $progress for url $u")
if (progress > prev) {
it.notify(progress)
prev = progress
if (progress >= 100) break
}
}
Log.d("MyDT", "quit comandy get progress, completed: $completed for url $u")
}.start()
} }
val r = ins.request(para)?.let { result ->
completed = true
p?.notify(100)
//Log.d("MyDT", "comandy reply: $result")
Gson().fromJson(result, ComandyCapsule::class.java)!!.let {
if (it.code != 200) throw IllegalArgumentException("HTTP${it.code} ${
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() }
}")
Base64.decode(it.data, Base64.DEFAULT)
}
}
completed = true
p?.notify(100)
r
} }
}.let { if(it?.isNotEmpty() == true ) return@withContext it } }.let { if(it?.isNotEmpty() == true ) return@withContext it }
failTimes.incrementAndGet()
} }
failTimes.incrementAndGet()
getApiConnection(u, "GET", refer, ua).let { getApiConnection(u, "GET", refer, ua).let {
val sz = it.getHeaderFieldInt("Content-Length", 0) val sz = it.getHeaderFieldInt("Content-Length", 0)
val ret = if (sz > 0 && p != null) { val ret = if (sz > 0 && p != null) {
@@ -144,7 +169,6 @@ object DownloadTools {
} }
it.disconnect() it.disconnect()
Log.d("MyDT", "getHttpContent: ${ret.size} bytes") Log.d("MyDT", "getHttpContent: ${ret.size} bytes")
failTimes.decrementAndGet()
ret ret
} }
} }
@@ -157,7 +181,6 @@ object DownloadTools {
task.get() task.get()
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace() ex.printStackTrace()
if (Comandy.instance.enabled) failTimes.incrementAndGet()
null null
} }
} }
@@ -167,12 +190,38 @@ object DownloadTools {
Log.d("MyDT", "prepareHttp: $u") Log.d("MyDT", "prepareHttp: $u")
FutureTask(if (!u.startsWith("https://copymanga.azurewebsites.net") && Comandy.instance.enabled) Callable{ FutureTask(if (!u.startsWith("https://copymanga.azurewebsites.net") && Comandy.instance.enabled) Callable{
try { try {
runBlocking { Comandy.instance.getInstance() }?.request(Gson().toJson( runBlocking { Comandy.instance.getInstance() }?.let { ins ->
getComandyNormalConnection(u, "GET", Config.pc_ua)) runBlocking {
)?.let { result -> val para = Gson().toJson(getComandyNormalConnection(u, "GET", Config.pc_ua))
Gson().fromJson(result, ComandyCapsule::class.java)?.let { var completed = false
if (it.code != 200) null p?.let {
else it.data?.let { d -> Base64.decode(d, Base64.DEFAULT) } Thread {
Log.d("MyDT", "launch comandy get progress, completed: $completed for url $u")
var prev = 0
while (!completed) {
sleep(50)
val progress = ins.progress(para)
Log.d("MyDT", "comandy get progress $progress for url $u")
if (progress > prev) {
it.notify(progress)
prev = progress
if (progress >= 100) break
}
}
Log.d("MyDT", "quit comandy get progress, completed: $completed for url $u")
}.start()
}
val r = ins.request(para)?.let { result ->
completed = true
p?.notify(100)
Gson().fromJson(result, ComandyCapsule::class.java)?.let {
if (it.code != 200) null
else it.data?.let { d -> Base64.decode(d, Base64.DEFAULT) }
}
}
completed = true
p?.notify(100)
r
} }
} }
} catch (ex: Exception) { } catch (ex: Exception) {
@@ -224,17 +273,16 @@ object DownloadTools {
} }
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace() ex.printStackTrace()
failTimes.incrementAndGet()
ex.message?.encodeToByteArray() ex.message?.encodeToByteArray()
} }
} }
else Callable { else Callable {
failTimes.incrementAndGet()
try { try {
getApiConnection(url, method, refer, ua).apply { getApiConnection(url, method, refer, ua).apply {
outputStream.write(body) outputStream.write(body)
ret = inputStream.readBytes() ret = inputStream.readBytes()
disconnect() disconnect()
failTimes.decrementAndGet()
} }
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace() ex.printStackTrace()

View File

@@ -79,11 +79,7 @@ open class AutoDownloadHandler(
try { try {
val data = Config.apiProxy?.comancry(url) { val data = Config.apiProxy?.comancry(url) {
DownloadTools.getHttpContent(it) DownloadTools.getHttpContent(it)
} }?:DownloadTools.getHttpContent(url)
if (data == null) {
delay(2000)
continue
}
if(exit) return@withContext if(exit) return@withContext
val fi = data.inputStream() val fi = data.inputStream()
val pass = setGsonItem(Gson().fromJson(fi.reader(), jsonClass)) val pass = setGsonItem(Gson().fromJson(fi.reader(), jsonClass))

View File

@@ -14,13 +14,9 @@ class PausableDownloader(private val url: String, private val waitMilliseconds:
var c = 0 var c = 0
while (!exit && c++ < 3) { while (!exit && c++ < 3) {
try { try {
val data = if (isApi) Config.apiProxy?.comancry(url) { val data = (if (isApi) Config.apiProxy?.comancry(url) {
DownloadTools.getHttpContent(it, Config.referer) DownloadTools.getHttpContent(it, Config.referer)
} else DownloadTools.getHttpContent(url, Config.referer) } else null)?:DownloadTools.getHttpContent(url, Config.referer)
if (data == null) {
delay(3000)
continue
}
whenFinish?.let { it(data) } whenFinish?.let { it(data) }
return@withContext true return@withContext true
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE resources [ <!DOCTYPE resources [
<!ENTITY hosturl "api.mangacopy.com"> <!ENTITY hosturl "www.copy-manga.com">
<!ENTITY appver "2.2.6"> <!ENTITY appver "2.2.6">
]> ]>
<resources> <resources>
@@ -164,11 +164,11 @@
<string name="settings_cat_net_sm_use_comandy">使用经过优化的请求方法访问服务器</string> <string name="settings_cat_net_sm_use_comandy">使用经过优化的请求方法访问服务器</string>
<string name="settings_cat_net_et_title_api_url">请求API网址</string> <string name="settings_cat_net_et_title_api_url">请求API网址</string>
<string name="settings_cat_net_et_summary_api_url">一般无需更改,除非拷贝漫画官方更改网址,默认:&hosturl;</string> <string name="settings_cat_net_et_summary_api_url">一般无需更改,除非拷贝漫画官方更改网址,默认:&hosturl;</string>
<string name="settings_cat_net_sw_use_api_proxy">使用API代理重启生效</string> <string name="settings_cat_net_sw_use_api_proxy">使用API代理需要密钥</string>
<string name="settings_cat_net_sm_use_api_proxy">作者自建的API代理可缓解国内图书详情加载问题但不保证100%解决,也不保证一直可用</string> <string name="settings_cat_net_sm_use_api_proxy">作者自建的API代理可缓解国内图书详情加载问题但不保证100%解决,也不保证一直可用</string>
<string name="settings_cat_net_sw_use_img_proxy">使用图床代理(重启生效</string> <string name="settings_cat_net_sw_use_img_proxy">使用图床代理(需要密钥</string>
<string name="settings_cat_net_sm_use_img_proxy">作者自建的图床代理可缓解国内图片无法加载问题但不保证100%解决,也不保证一直可用</string> <string name="settings_cat_net_sm_use_img_proxy">作者自建的图床代理可缓解国内图片无法加载问题但不保证100%解决,也不保证一直可用</string>
<string name="settings_cat_net_et_title_img_proxy">代理密钥(重启生效)</string> <string name="settings_cat_net_et_title_img_proxy">代理密钥</string>
<string name="settings_cat_net_et_summary_img_proxy">为避免滥用该密钥需加群559748702获得且随时可能会刷新</string> <string name="settings_cat_net_et_summary_img_proxy">为避免滥用该密钥需加群559748702获得且随时可能会刷新</string>
<string name="settings_cat_vm">漫画浏览</string> <string name="settings_cat_vm">漫画浏览</string>