Files
xiaoli-cli/operit_py/jmcomic.py
T
shuiyu1123 dc77dfd304 feat: 将 47 个 Operit TypeScript 脚本全面翻译为 Python
- 新增 operit_runtime.py: 跨平台 Python 运行时
  - 实现 Tools.System/Files/Net/UI/Memory/FFmpeg/Workflow/Chat/SoftwareSettings/Tasker
  - 实现 OkHttp 链式 HTTP 客户端兼容层
  - 实现 complete()/exports/console 全局对象
  - 实现 OperitScriptLoader 脚本加载器
  - 支持 Windows/macOS/Linux 三平台

- 新增 operit_py/ 目录: 47 个 Python 翻译脚本
  - 搜索类: duckduckgo, google_search, tavily, zhipu_search, various_search, crossref
  - 绘图类: openai_draw, qwen_draw, xai_draw, minimax_draw, siliconflow_draw, zhipu_draw, nanobanana_draw
  - 媒体类: ffmpeg, file_converter, douyin_download, bilibili_tools, jmcomic
  - 文件类: extended_file_tools, hex_editor, reader, beeimg_image_uploader
  - 网络类: extended_http_tools, network_test
  - 自动化类: browser, automatic_ui_base, automatic_ui_subagent, automatic_bilibili_assistant, automatic_xiaohongshu_assistant, automatic_baidu_map_assistant
  - 系统类: system_tools, super_admin, daily_life, operit_editor
  - 对话类: ai_chat, extended_chat, qq_intelligent
  - 工作流类: workflow, vflow_trigger, tasker
  - 工具类: time, code_runner, quick_start, extended_memory_tools, baidu_map, 12306, pdf_vision_parser

- 测试结果: 47/47 语法检查通过, 38 脚本加载成功, 280 个工具注册
- Android 专属功能已降级处理(保留接口,返回不支持提示)
2026-06-23 12:27:18 +00:00

703 lines
28 KiB
Python

# METADATA
# {
# "name": "jmcomic_downloader",
# "display_name": {"zh": "JMComic 下载器", "en": "JMComic Downloader"},
# "description": {"zh": "提供JMComic漫画下载功能,支持搜索、获取信息和下载,包括对新漫画的图片反爬解码。", "en": "JMComic downloader: search comics, fetch details, and download albums. Includes anti-crawling image decoding for newer comics."},
# "category": "Media",
# "tools": [
# {"name": "main", "description": {"zh": "运行一个内置的测试函数,以验证JMComic工具的基本功能(搜索和获取信息)是否正常工作。", "en": "Run a built-in test to verify basic JMComic functionality (search and info retrieval)."}, "parameters": []},
# {"name": "search_comics", "description": {"zh": "搜索JMComic漫画", "en": "Search JMComic comics."}, "parameters": [
# {"name": "query", "description": {"zh": "搜索关键词", "en": "Search keyword."}, "type": "string", "required": True},
# {"name": "page", "description": {"zh": "页码 (默认: 1)", "en": "Page number (default: 1)."}, "type": "number", "required": False},
# {"name": "order_by", "description": {"zh": "排序方式 (latest, view, picture, like, 默认: view)", "en": "Sort mode: latest/view/picture/like (default: view)."}, "type": "string", "required": False},
# {"name": "time", "description": {"zh": "时间范围 (today, week, month, all, 默认: all)", "en": "Time range: today/week/month/all (default: all)."}, "type": "string", "required": False}
# ]},
# {"name": "get_album_info", "description": {"zh": "获取漫画(本子)的详细信息", "en": "Get detailed information for a comic album."}, "parameters": [
# {"name": "album_id", "description": {"zh": "漫画ID", "en": "Album ID."}, "type": "string", "required": True}
# ]},
# {"name": "download_album", "description": {"zh": "下载指定ID的单本漫画,包含图片解码功能。", "en": "Download a single comic album by ID, including image decoding."}, "parameters": [
# {"name": "album_id", "description": {"zh": "要下载的漫画ID", "en": "Album ID to download."}, "type": "string", "required": True},
# {"name": "download_dir", "description": {"zh": "下载目录 (可选, 默认: /sdcard/Download/Operit/plugins/jmcomic_downloader/downloads) (Android-only 路径)", "en": "Download directory (optional)."}, "type": "string", "required": False}
# ]},
# {"name": "batch_download_albums", "description": {"zh": "批量下载多本漫画,包含图片解码功能。", "en": "Batch download multiple comic albums, including image decoding."}, "parameters": [
# {"name": "album_ids", "description": {"zh": "要下载的漫画ID列表,用逗号分隔", "en": "Comma-separated list of album IDs to download."}, "type": "string", "required": True},
# {"name": "download_dir", "description": {"zh": "下载目录 (可选)", "en": "Download directory (optional)."}, "type": "string", "required": False}
# ]}
# ],
# "enabledByDefault": False
# }
import json
import re
import time
import math
import random
import hashlib
import base64
import asyncio
from urllib.parse import quote, unquote
Error = Exception
# AES 解密依赖(跨平台)
try:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding as _padding
_HAS_CRYPTO = True
except ImportError:
_HAS_CRYPTO = False
# 图片处理依赖(跨平台,替代 Jimp)
try:
from PIL import Image
import io as _io
_HAS_PIL = True
except ImportError:
_HAS_PIL = False
DEFAULT_DOWNLOAD_DIR = f"{getPluginConfigDir('jmcomic_downloader')}/downloads"
TEST_DOWNLOAD_DIR = f"{getPluginConfigDir('jmcomic_downloader')}/test_downloads"
__version__ = '2.6.4-py-adapted'
def shuffleDomains(domains):
shuffled = list(domains)
for i in range(len(shuffled) - 1, 0, -1):
j = int(random.random() * (i + 1))
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
return shuffled
JmMagicConstants = {
"APP_TOKEN_SECRET": '18comicAPP',
"APP_TOKEN_SECRET_2": '18comicAPPContent',
"APP_DATA_SECRET": '185Hcomic3PAPP7R',
"APP_VERSION": '1.8.0',
"SCRAMBLE_220980": 220980,
"SCRAMBLE_268850": 268850,
"SCRAMBLE_421926": 421926,
}
JmModuleConfig = {
"PROT": 'https://',
"DOMAIN_API_LIST": shuffleDomains([
'www.cdnmhwscc.vip',
'www.cdnplaystation6.club',
'www.cdnplaystation6.org',
'www.cdnuc.vip',
'www.cdn-mspjmapiproxy.xyz',
]),
"DOMAIN_IMAGE_LIST": shuffleDomains([
'cdn-msp.jmapiproxy1.cc',
'cdn-msp.jmapiproxy2.cc',
'cdn-msp2.jmapiproxy2.cc',
'cdn-msp3.jmapiproxy2.cc',
]),
"APP_HEADERS_TEMPLATE": {
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'X-Requested-With': 'com.jiaohua_browser',
'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.11211812; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36',
},
"APP_HEADERS_IMAGE": {
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
'X-Requested-With': 'com.jiaohua_browser',
},
}
def joinPath(*segments):
return re.sub(r'/+', '/', '/'.join(segments))
def dirname(filePath):
lastSlashPos = filePath.rfind('/')
if lastSlashPos == -1:
return "."
if lastSlashPos == 0:
return "/"
return filePath[:lastSlashPos]
def basename(filePath):
return filePath[filePath.rfind('/') + 1:]
async def ensureDirExists(dirPath):
if not dirPath or dirPath == '/' or dirPath == '.':
return
dirExists = await Tools.Files.exists(dirPath)
if dirExists.exists:
return
parentDir = dirname(dirPath)
await ensureDirExists(parentDir)
dirStillNotExists = await Tools.Files.exists(dirPath)
if not dirStillNotExists.exists:
await Tools.Files.mkdir(dirPath)
async def runTasksWithConcurrency(tasks, limit):
results = [None] * len(tasks)
currentIndex = [0]
async def runner():
while currentIndex[0] < len(tasks):
taskIndex = currentIndex[0]
currentIndex[0] += 1
if taskIndex < len(tasks):
try:
results[taskIndex] = await tasks[taskIndex]()
except Exception as e:
console.error(f"并发任务 {taskIndex} 执行失败: {str(e)}")
results[taskIndex] = e
numRunners = min(limit, len(tasks))
await asyncio.gather(*[runner() for _ in range(numRunners)])
return [r for r in results if not isinstance(r, Exception)]
class JmImageTool:
@staticmethod
def getNum(scrambleId, photoId, imageName):
scrambleIdNum = int(str(scrambleId))
photoIdNum = int(str(photoId))
if photoIdNum < scrambleIdNum:
return 0
elif photoIdNum < JmMagicConstants["SCRAMBLE_268850"]:
return 10
else:
x = 10 if photoIdNum < JmMagicConstants["SCRAMBLE_421926"] else 8
imageNameWithoutExt = JmImageTool.getFileNameFromUrl(imageName, True)
s = f"{photoIdNum}{imageNameWithoutExt}"
hash_str = hashlib.md5(s.encode('utf-8')).hexdigest()
lastChar = ord(hash_str[-1])
num = lastChar % x
return (num * 2) + 2
@staticmethod
def getFileNameFromUrl(url, withoutExtension=True):
queryIndex = url.find('?')
if queryIndex != -1:
url = url[:queryIndex]
filename = basename(url)
if withoutExtension:
lastDotIndex = filename.rfind('.')
return filename[:lastDotIndex] if lastDotIndex != -1 else filename
return filename
@staticmethod
async def decodeAndSave(num, imageBase64, decodedSavePath):
if num == 0:
await Tools.Files.writeBinary(decodedSavePath, imageBase64)
return
if not _HAS_PIL:
console.warn("PIL/Pillow 不可用,跳过图片解码,保存原始图片。")
await Tools.Files.writeBinary(decodedSavePath, imageBase64)
return
try:
raw_bytes = base64.b64decode(imageBase64)
srcImage = Image.open(_io.BytesIO(raw_bytes))
w, h = srcImage.size
over = h % num
resultImage = Image.new(srcImage.mode, (w, h))
for i in range(num):
move = int(h / num)
ySrc = h - (move * (i + 1)) - over
yDst = move * i
if i == 0:
move += over
else:
yDst += over
if ySrc < 0 or move <= 0 or (ySrc + move > h):
continue
strip = srcImage.crop((0, ySrc, w, ySrc + move))
resultImage.paste(strip, (0, yDst))
buf = _io.BytesIO()
resultImage.save(buf, format='JPEG')
pureBase64 = base64.b64encode(buf.getvalue()).decode('ascii')
await Tools.Files.writeBinary(decodedSavePath, pureBase64)
except Exception as e:
console.error(f"图片解码失败,将保存原始图片: {str(e)}")
await Tools.Files.writeBinary(decodedSavePath, imageBase64)
class JmCryptoTool:
@staticmethod
def md5hex(key):
return hashlib.md5(key.encode('utf-8')).hexdigest()
@staticmethod
def tokenAndTokenparam(ts, secret=None):
if secret is None:
secret = JmMagicConstants["APP_TOKEN_SECRET"]
tokenparam = f"{ts},{JmMagicConstants['APP_VERSION']}"
token = JmCryptoTool.md5hex(f"{ts}{secret}")
return [token, tokenparam]
@staticmethod
def decodeRespData(data, ts, secret=None):
if secret is None:
secret = JmMagicConstants["APP_DATA_SECRET"]
try:
keyHex = JmCryptoTool.md5hex(f"{ts}{secret}")
if not _HAS_CRYPTO:
raise Error("cryptography 库不可用,无法进行 AES 解密。")
key = bytes.fromhex(keyHex)
cipher = Cipher(algorithms.AES(key), modes.ECB())
decryptor = cipher.decryptor()
# data 是 base64 编码的密文
ciphertext = base64.b64decode(data)
decrypted_padded = decryptor.update(ciphertext) + decryptor.finalize()
# 移除 PKCS7 填充
unpadder = _padding.PKCS7(128).unpadder()
decrypted = unpadder.update(decrypted_padded) + unpadder.finalize()
decryptedText = decrypted.decode('utf-8')
if not decryptedText:
raise Error("AES decryption returned an empty result.")
return decryptedText
except Exception as error:
if isinstance(error, Error):
raise error
raise Error(f"AES Decryption failed. Original error: {str(error)}")
class JmApiResp:
def __init__(self, resp, ts):
self.resp = resp
self.ts = ts
@property
def isSuccess(self):
return self.resp.isSuccessful()
@property
def json(self):
try:
return json.loads(self.resp.content)
except Exception as error:
raise Error(f"JSON解析失败: {str(error)}")
@property
def isSuccessful(self):
return self.isSuccess and self.json.get("code") == 200
@property
def encodedData(self):
return self.json.get("data")
@property
def decodedData(self):
return JmCryptoTool.decodeRespData(self.encodedData, self.ts)
@property
def resData(self):
if not self.isSuccessful:
raise Error(f"API请求失败: code={self.json.get('code')}")
decoded = self.decodedData
try:
if not isinstance(decoded, str) or not decoded:
raise Error(f"Cannot parse non-string or empty value. Type: {type(decoded).__name__}")
return json.loads(decoded)
except Exception as error:
if isinstance(error, Error):
raise error
preview = str(decoded or 'N/A')[:80]
raise Error(f"Failed to parse decrypted response. Error: {str(error)}.")
@property
def modelData(self):
return self.resData
class DirRuleImpl:
def __init__(self, baseDir):
self.baseDir = baseDir
def decideImageSaveDir(self, album, photo):
return joinPath(self.baseDir, self.sanitize(album["title"]))
def decideAlbumRootDir(self, album):
return joinPath(self.baseDir, self.sanitize(album["title"]))
def sanitize(self, name):
return re.sub(r'[\\?%*:|"<>]', '_', name)
class JmOptionImpl:
def __init__(self, baseDir=None):
if baseDir is None:
baseDir = DEFAULT_DOWNLOAD_DIR
self.dirRule = DirRuleImpl(baseDir)
@staticmethod
def default(baseDir=None):
return JmOptionImpl(baseDir)
def buildJmClient(self):
return JmApiClientImpl()
class JmApiClientImpl:
def __init__(self):
self.domainList = JmModuleConfig["DOMAIN_API_LIST"]
self.retryTimes = 3
self.client = OkHttp.newClient()
self.API_ALBUM = '/album'
self.API_CHAPTER = '/chapter'
self.API_SEARCH = '/search'
self.API_CATEGORIES_FILTER = '/categories/filter'
async def getAlbumDetail(self, albumId):
resp = await self.reqApi(f"{self.API_ALBUM}?id={albumId}")
data = resp.resData
if not data or not data.get("name"):
raise Error(f"本子 {albumId} 不存在或数据无效")
return self.parseAlbumData(albumId, data)
async def getPhotoDetail(self, photoId):
resp = await self.reqApi(f"{self.API_CHAPTER}?id={photoId}")
data = resp.resData
if not data or not data.get("name"):
raise Error(f"章节 {photoId} 不存在或数据无效")
return self.parsePhotoData(photoId, data)
async def searchComics(self, params):
query = params.get("query")
page = params.get("page", 1)
order_by = params.get("order_by", "view")
time_range = params.get("time", "all")
orderMap = {"latest": "mr", "view": "mv", "picture": "mp", "like": "tf"}
timeMap = {"today": "t", "week": "w", "month": "m", "all": "a"}
apiParams = {
"search_query": query,
"page": page,
"o": orderMap.get(order_by.lower(), orderMap["view"]),
"t": timeMap.get(time_range.lower(), timeMap["all"]),
}
resp = await self.reqApi(f"{self.API_SEARCH}?{self.toUrlSearchParams(apiParams)}")
data = resp.resData
results = [
{"id": str(item.get("id") or item.get("album_id")), "title": item.get("name") or item.get("title")}
for item in (data.get("content") or [])
]
return {
"search_params": params,
"results": results,
"total_results": len(results),
}
async def reqApi(self, url, method="GET", data=None):
ts = int(time.time())
for i in range(len(self.domainList)):
domain = self.domainList[i]
for retry in range(self.retryTimes):
try:
fullUrl = f"{JmModuleConfig['PROT']}{domain}{url}"
token, tokenparam = JmCryptoTool.tokenAndTokenparam(ts)
headers = dict(JmModuleConfig["APP_HEADERS_TEMPLATE"])
headers["token"] = token
headers["tokenparam"] = tokenparam
requestBuilder = self.client.newRequest().url(fullUrl).headers(headers)
if method == "POST":
requestBuilder.method("POST").jsonBody(data)
resp = await requestBuilder.build().execute()
if resp.isSuccessful():
return JmApiResp(resp, ts)
except Exception as error:
console.log(f"[API] 请求失败: {str(error)} 域名: {domain}")
if retry == self.retryTimes - 1 and i == len(self.domainList) - 1:
raise Error(f"所有域名和重试都失败: {str(error)}")
raise Error("请求失败")
async def downloadImage(self, imageUrl, savePath, scrambleId, photoId):
try:
response = await self.client.newRequest().url(imageUrl).headers(JmModuleConfig["APP_HEADERS_IMAGE"]).build().execute()
if not response.isSuccessful():
raise Error(f"HTTP error! status: {response.statusCode}")
imageBase64 = response.bodyAsBase64()
dir_ = dirname(savePath)
await ensureDirExists(dir_)
imageName = JmImageTool.getFileNameFromUrl(imageUrl, False)
num = JmImageTool.getNum(scrambleId, photoId, imageName)
await JmImageTool.decodeAndSave(num, imageBase64, savePath)
return True
except Exception as error:
console.error(f"[图片] 下载失败: {imageUrl}, 错误: {str(error)}")
return False
def toUrlSearchParams(self, obj):
return '&'.join([f"{quote(str(k))}={quote(str(obj[k]))}" for k in obj])
def parseAlbumData(self, albumId, data):
episodeList = data.get("series") if data.get("series") and len(data.get("series")) > 0 else [{"id": albumId, "title": data.get("name")}]
return {
"id": albumId,
"title": data.get("name") or f"本子 {albumId}",
"author": (data.get("author") and data["author"][0]) or "未知作者",
"episodeList": episodeList,
"scrambleId": data.get("scramble_id") or JmMagicConstants["SCRAMBLE_220980"],
"length": len(episodeList),
}
def parsePhotoData(self, photoId, data):
return {
"id": photoId,
"title": data.get("name") or f"章节 {photoId}",
"pageArr": data.get("images") or [],
"albumId": data.get("album_id") or photoId,
"scrambleId": data.get("scramble_id") or JmMagicConstants["SCRAMBLE_220980"],
"length": len(data.get("images") or []),
}
class JmDownloaderImpl:
def __init__(self, option):
self.option = option
self.client = option.buildJmClient()
async def downloadAlbum(self, albumId):
album = await self.client.getAlbumDetail(albumId)
await self.downloadByAlbumDetail(album)
return album
async def downloadByAlbumDetail(self, album):
albumDir = self.option.dirRule.decideAlbumRootDir(album)
await ensureDirExists(albumDir)
console.log(f"[专辑: {album['title']}] 发现 {album['episodeList'].__len__()} 个章节, 开始下载...")
chapterConcurrency = 5
def make_task(episode, i):
async def _task():
console.log(f" [章节 {i + 1}/{len(album['episodeList'])}] 开始下载: {episode['title']} ({episode['id']})")
try:
photo = await self.client.getPhotoDetail(episode["id"])
await self.downloadPhotoImages(photo, albumDir, album["id"])
console.log(f" [章节 {i + 1}/{len(album['episodeList'])}] 下载完成: {episode['title']}")
except Exception as e:
console.error(f" [章节 {i + 1}/{len(album['episodeList'])}] 下载失败: {episode['title']}, 错误: {str(e)}")
return _task
tasks = [make_task(episode, i) for i, episode in enumerate(album["episodeList"])]
await runTasksWithConcurrency(tasks, chapterConcurrency)
async def downloadPhotoImages(self, photo, albumDir, albumId):
if not photo["pageArr"] or len(photo["pageArr"]) == 0:
return
console.log(f" [图片集: {photo['title']}] 发现 {len(photo['pageArr'])} 张图片, 开始下载...")
concurrencyLimit = 10
def make_task(imageName, i):
async def _task():
finalFileName = f"{str(i + 1).zfill(5)}.jpg"
filePath = joinPath(albumDir, finalFileName)
fileExists = await Tools.Files.exists(filePath)
if fileExists.exists:
return
imageUrl = self.buildImageUrl(photo, imageName)
try:
await self.client.downloadImage(imageUrl, filePath, photo["scrambleId"], photo["id"])
except Exception as e:
console.error(f" [图片下载失败] {finalFileName} from {photo['title']}: {str(e)}")
return _task
tasks = [make_task(imageName, i) for i, imageName in enumerate(photo["pageArr"])]
await runTasksWithConcurrency(tasks, concurrencyLimit)
console.log(f" [图片集: {photo['title']}] 所有图片下载任务已处理。")
def buildImageUrl(self, photo, imageName):
domain = JmModuleConfig["DOMAIN_IMAGE_LIST"][int(random.random() * len(JmModuleConfig["DOMAIN_IMAGE_LIST"]))]
return f"{JmModuleConfig['PROT']}{domain}/media/photos/{photo['albumId']}/{imageName}"
class SimpleJMDownloader:
def __init__(self, downloadDir=None):
if downloadDir is None:
downloadDir = DEFAULT_DOWNLOAD_DIR
self.option = JmOptionImpl.default(downloadDir)
self.downloader = JmDownloaderImpl(self.option)
self.client = self.option.buildJmClient()
console.log(f"JM下载器初始化成功, 下载目录: {self.option.dirRule.baseDir}")
async def searchComics(self, params):
console.log(f"搜索漫画: {params.get('query')}")
return await self.client.searchComics(params)
async def getAlbumInfo(self, albumId):
album = await self.client.getAlbumDetail(albumId)
return {
"id": album["id"],
"title": album["title"],
"author": album["author"],
"chapterCount": album["length"],
"success": True,
}
async def downloadAlbum(self, albumId):
console.log(f"获取本子信息: {albumId}")
try:
info = await self.getAlbumInfo(albumId)
if not info["success"]:
return {"success": False, "albumId": albumId, "error": "获取信息失败"}
console.log(f"开始下载本子: {info['title']}")
await self.downloader.downloadAlbum(albumId)
downloadedFiles = await self._checkDownloadedFiles(info["title"])
return {
"success": True,
"albumId": albumId,
"title": info["title"],
"downloadedFiles": downloadedFiles,
}
except Exception as error:
return {"success": False, "albumId": albumId, "error": str(error)}
async def batchDownload(self, albumIds):
results = []
console.log(f"开始批量下载 {len(albumIds)} 个本子")
concurrencyLimit = 3
def make_task(albumId, i):
async def _task():
console.log(f"\n[{i + 1}/{len(albumIds)}] 开始处理本子: {albumId}")
result = await self.downloadAlbum(albumId)
if result["success"]:
console.log(f"[{i + 1}/{len(albumIds)}] 下载成功: {result.get('title')}")
else:
console.log(f"[{i + 1}/{len(albumIds)}] 下载失败: {albumId}, {result.get('error') or 'Unknown error'}")
return result
return _task
tasks = [make_task(albumId, i) for i, albumId in enumerate(albumIds)]
return await runTasksWithConcurrency(tasks, concurrencyLimit)
async def _checkDownloadedFiles(self, title):
albumDir = self.option.dirRule.decideAlbumRootDir({"title": title})
dirExists = await Tools.Files.exists(albumDir)
if dirExists.exists:
listResult = await Tools.Files.list(albumDir)
entries = getattr(listResult, "entries", None) or []
files = [e.name for e in entries]
return {
"directory": albumDir,
"fileCount": len(files),
"files": files[:10],
}
return {"directory": None, "fileCount": 0, "files": []}
async def main():
console.log("开始执行JMComic工具功能测试...")
downloader = SimpleJMDownloader(TEST_DOWNLOAD_DIR)
testQuery = "原神"
console.log(f'1. 测试搜索功能,关键词: "{testQuery}"')
searchResult = await downloader.searchComics({"query": testQuery})
if not searchResult or not searchResult.get("results") or len(searchResult["results"]) == 0:
raise Error(f'搜索测试失败: 未能找到关于 "{testQuery}" 的任何结果。')
console.log(f"搜索成功, 找到 {searchResult['total_results']} 个结果。")
firstAlbum = searchResult["results"][0]
console.log(f"2. 测试获取作品信息功能, 作品ID: {firstAlbum['id']} ({firstAlbum['title']})")
albumInfo = await downloader.getAlbumInfo(firstAlbum["id"])
if not albumInfo or not albumInfo["success"]:
raise Error(f"获取作品信息失败, ID: {firstAlbum['id']}")
console.log(f"作品信息获取成功:")
console.log(f" - 标题: {albumInfo['title']}")
console.log(f" - 作者: {albumInfo['author']}")
console.log(f" - 章节数: {albumInfo['chapterCount']}")
console.log(f"3. 测试下载功能, 作品ID: {firstAlbum['id']} ({firstAlbum['title']})")
downloadResult = await downloader.downloadAlbum(firstAlbum["id"])
if not downloadResult or not downloadResult["success"]:
raise Error(f"下载作品失败, ID: {firstAlbum['id']}")
console.log(f"下载成功:")
dl = downloadResult.get("downloadedFiles") or {}
console.log(f" - 保存目录: {dl.get('directory')}")
console.log(f" - 文件数量: {dl.get('fileCount')}")
summary = f"JMComic工具测试完成。成功搜索、获取信息并下载了作品《{albumInfo['title']}》。"
console.log(f"\n{summary}")
return summary
async def search_comics(params):
downloader = SimpleJMDownloader()
return await downloader.searchComics(params)
async def get_album_info(params):
downloader = SimpleJMDownloader()
return await downloader.getAlbumInfo(params.get("album_id"))
async def download_album(params):
downloader = SimpleJMDownloader(params.get("download_dir"))
return await downloader.downloadAlbum(params.get("album_id"))
async def batch_download_albums(params):
albumIds = [id_.strip() for id_ in params.get("album_ids", "").split(",") if id_.strip()]
if len(albumIds) == 0:
raise Error("album_ids不能为空")
downloader = SimpleJMDownloader(params.get("download_dir"))
return await downloader.batchDownload(albumIds)
async def jmcomic_wrap(func, params, successMessage, failMessage):
try:
console.log(f"开始执行: {func.__name__}")
result = await func(params)
complete({"success": True, "message": successMessage, "data": result})
except Exception as error:
import traceback
console.error(f"{func.__name__} 执行失败: {str(error)}")
complete({"success": False, "message": f"{failMessage}: {str(error)}", "error_stack": traceback.format_exc()})
async def _main_wrapper(params):
await jmcomic_wrap(main, params, '功能测试完成', '功能测试失败')
async def _search_comics_wrapper(params):
await jmcomic_wrap(search_comics, params, '搜索完成', '搜索失败')
async def _get_album_info_wrapper(params):
await jmcomic_wrap(get_album_info, params, '信息获取完成', '信息获取失败')
async def _download_album_wrapper(params):
await jmcomic_wrap(download_album, params, '下载完成', '下载失败')
async def _batch_download_albums_wrapper(params):
await jmcomic_wrap(batch_download_albums, params, '批量下载完成', '批量下载失败')
exports.main = _main_wrapper
exports.search_comics = _search_comics_wrapper
exports.get_album_info = _get_album_info_wrapper
exports.download_album = _download_album_wrapper
exports.batch_download_albums = _batch_download_albums_wrapper