Skip to content

Commit f35cb12

Browse files
authored
v2.4.3: 实现最新的禁漫APP接口加解密算法(1.6.3),优化代码结构,更新README (#167)
1 parent 52298d0 commit f35cb12

File tree

8 files changed

+216
-146
lines changed

8 files changed

+216
-146
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
# Python API For JMComic (禁漫天堂)
22

3-
封装了一套可用于爬取JM的Python API.
3+
本项目封装了一套可用于爬取JM的Python API.
44

5-
简单来说,就是可以通过简单的几行Python代码,实现下载JM上的本子到本地,并且是处理好的图片.
5+
你可以通过简单的几行Python代码,实现下载JM上的本子到本地,并且是处理好的图片
66

77
**友情提示:珍爱JM,为了减轻JM的服务器压力,请不要一次性爬取太多本子,西门🙏🙏🙏**.
88

9+
## 项目介绍
10+
11+
本项目的核心功能是下载本子,基于此,设计了一套方便使用、便于扩展,能满足一些特殊下载需求的框架。
12+
13+
除了下载功能以外,也实现了其他的一些禁漫接口,例如登录、搜索、收藏夹等等,按需实现。
14+
15+
目前核心功能实现较为稳定,项目也处于维护阶段(因为禁漫接口经常变动,需要经常维护)。
16+
17+
918
## 安装教程
1019

1120
* 通过pip官方源安装(推荐,并且更新也是这个命令)
@@ -38,6 +47,7 @@ $ jmcomic 422866
3847
## 项目特点
3948

4049
- **绕过Cloudflare的反爬虫**
50+
- **实现禁漫APP接口最新的加解密算法 (1.6.3)**
4151
- 用法多样:
4252

4353
- GitHub Actions:网页上直接输入本子id就能下载([教程:使用GitHub Actions下载禁漫本子](./assets/docs/sources/tutorial/1_github_actions.md)

src/jmcomic/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# 被依赖方 <--- 使用方
33
# config <--- entity <--- toolkit <--- client <--- option <--- downloader
44

5-
__version__ = '2.4.2'
5+
__version__ = '2.4.3'
66

77
from .api import *
88
from .jm_plugin import *

src/jmcomic/jm_client_impl.py

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ def album_comment(self,
392392
)
393393

394394
resp = self.post('/ajax/album_comment',
395-
headers=JmModuleConfig.album_comment_headers,
395+
headers=self.album_comment_headers,
396396
data=data,
397397
)
398398

@@ -467,6 +467,26 @@ def check_special_http_code(cls, resp):
467467
+ (f'URL=[{url}]' if url is not None else '')
468468
)
469469

470+
album_comment_headers = {
471+
'authority': '18comic.vip',
472+
'accept': 'application/json, text/javascript, */*; q=0.01',
473+
'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
474+
'cache-control': 'no-cache',
475+
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
476+
'origin': 'https://18comic.vip',
477+
'pragma': 'no-cache',
478+
'referer': 'https://18comic.vip/album/248965/',
479+
'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
480+
'sec-ch-ua-mobile': '?0',
481+
'sec-ch-ua-platform': '"Windows"',
482+
'sec-fetch-dest': 'empty',
483+
'sec-fetch-mode': 'cors',
484+
'sec-fetch-site': 'same-origin',
485+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
486+
'Chrome/114.0.0.0 Safari/537.36',
487+
'x-requested-with': 'XMLHttpRequest',
488+
}
489+
470490

471491
# 基于禁漫移动端(APP)实现的JmClient
472492
class JmApiClient(AbstractJmClient):
@@ -556,7 +576,7 @@ def fetch_detail_entity(self, apid, clazz):
556576
url,
557577
params={
558578
'id': apid,
559-
}
579+
},
560580
)
561581

562582
self.require_resp_success(resp, url)
@@ -571,11 +591,13 @@ def fetch_scramble_id(self, photo_id):
571591
resp = self.req_api(
572592
self.API_SCRAMBLE,
573593
params={
574-
"id": photo_id,
575-
"mode": "vertical",
576-
"page": "0",
577-
"app_img_shunt": "1",
578-
}
594+
'id': photo_id,
595+
'mode': 'vertical',
596+
'page': '0',
597+
'app_img_shunt': '1',
598+
'express': 'off',
599+
'v': time_stamp(),
600+
},
579601
)
580602

581603
scramble_id = PatternTool.match_or_default(resp.text,
@@ -713,21 +735,41 @@ def favorite_folder(self,
713735
return JmPageTool.parse_api_to_favorite_page(resp.model_data)
714736

715737
def req_api(self, url, get=True, **kwargs) -> JmApiResp:
716-
# set headers
717-
headers, key_ts = self.headers_key_ts
718-
kwargs['headers'] = headers
738+
ts = self.decide_headers_and_ts(kwargs, url)
719739

720740
if get:
721741
resp = self.get(url, **kwargs)
722742
else:
723743
resp = self.post(url, **kwargs)
724744

725-
return JmApiResp.wrap(resp, key_ts)
745+
return JmApiResp(resp, ts)
746+
747+
# noinspection PyMethodMayBeStatic
748+
def decide_headers_and_ts(self, kwargs, url):
749+
# 获取时间戳
750+
if url == self.API_SCRAMBLE:
751+
# /chapter_view_template
752+
# 这个接口很特殊,用的密钥 18comicAPPContent 而不是 18comicAPP
753+
# 如果用后者,则会返回403信息
754+
ts = time_stamp()
755+
token, tokenparam = JmCryptoTool.token_and_tokenparam(ts, secret=JmMagicConstants.APP_TOKEN_SECRET_2)
756+
757+
elif JmModuleConfig.use_fix_timestamp:
758+
ts, token, tokenparam = JmModuleConfig.get_fix_ts_token_tokenparam()
759+
760+
else:
761+
ts = time_stamp()
762+
token, tokenparam = JmCryptoTool.token_and_tokenparam(ts)
763+
764+
# 计算token,tokenparam
765+
headers = kwargs.get('headers', JmMagicConstants.APP_HEADERS_TEMPLATE.copy())
766+
headers.update({
767+
'token': token,
768+
'tokenparam': tokenparam,
769+
})
770+
kwargs['headers'] = headers
726771

727-
@property
728-
def headers_key_ts(self):
729-
key_ts = time_stamp()
730-
return JmModuleConfig.new_api_headers(key_ts), key_ts
772+
return ts
731773

732774
@classmethod
733775
def require_resp_success(cls, resp: JmApiResp, orig_req_url: str):
@@ -743,11 +785,22 @@ def require_resp_success(cls, resp: JmApiResp, orig_req_url: str):
743785
# 暂无
744786

745787
def after_init(self):
746-
# cookies = self.__class__.fetch_init_cookies(self)
747-
# self.get_root_postman().get_meta_data()['cookies'] = cookies
788+
# 保证拥有cookies,因为移动端要求必须携带cookies,否则会直接跳转同一本子【禁漫娘】
789+
if JmModuleConfig.api_client_require_cookies:
790+
self.ensure_have_cookies()
748791

749-
self.get_root_postman().get_meta_data()['cookies'] = JmModuleConfig.get_cookies(self)
750-
pass
792+
from threading import Lock
793+
client_init_cookies_lock = Lock()
794+
795+
def ensure_have_cookies(self):
796+
if self.get_meta_data('cookies'):
797+
return
798+
799+
with self.client_init_cookies_lock:
800+
if self.get_meta_data('cookies'):
801+
return
802+
803+
self['cookies'] = JmModuleConfig.get_cookies(self)
751804

752805

753806
class FutureClientProxy(JmcomicClient):

src/jmcomic/jm_client_interface.py

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -68,45 +68,21 @@ def transfer_to(self,
6868

6969
class JmApiResp(JmResp):
7070

71-
@classmethod
72-
def wrap(cls, resp, key_ts):
71+
def __init__(self, resp, ts: str):
7372
ExceptionTool.require_true(not isinstance(resp, JmApiResp), f'重复包装: {resp}')
7473

75-
return cls(resp, key_ts)
76-
77-
def __init__(self, resp, key_ts):
7874
super().__init__(resp)
79-
self.key_ts = key_ts
75+
self.ts = ts
8076
self.cache_decode_data = None
8177

8278
@property
8379
def is_success(self) -> bool:
8480
return super().is_success and self.json()['code'] == 200
8581

86-
@staticmethod
87-
def parse_data(text, time) -> str:
88-
# 1. base64解码
89-
import base64
90-
data = base64.b64decode(text)
91-
92-
# 2. AES-ECB解密
93-
# key = 时间戳拼接 '18comicAPPContent' 的md5
94-
import hashlib
95-
key = hashlib.md5(f"{time}18comicAPPContent".encode("utf-8")).hexdigest().encode("utf-8")
96-
from Crypto.Cipher import AES
97-
data = AES.new(key, AES.MODE_ECB).decrypt(data)
98-
99-
# 3. 移除末尾的一些特殊字符
100-
data = data[:-data[-1]]
101-
102-
# 4. 解码为字符串 (json)
103-
res = data.decode('utf-8')
104-
return res
105-
10682
@property
10783
@field_cache('__cache_decoded_data__')
10884
def decoded_data(self) -> str:
109-
return self.parse_data(self.encoded_data, self.key_ts)
85+
return JmCryptoTool.decode_resp_data(self.encoded_data, self.ts)
11086

11187
@property
11288
def encoded_data(self) -> str:

0 commit comments

Comments
 (0)