jmcomic 2.5.5__tar.gz → 2.5.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {jmcomic-2.5.5/src/jmcomic.egg-info → jmcomic-2.5.7}/PKG-INFO +31 -18
  2. {jmcomic-2.5.5 → jmcomic-2.5.7}/README.md +30 -17
  3. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/__init__.py +1 -1
  4. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/api.py +15 -4
  5. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/jm_client_impl.py +18 -16
  6. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/jm_client_interface.py +13 -12
  7. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/jm_config.py +52 -16
  8. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/jm_downloader.py +75 -23
  9. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/jm_entity.py +11 -7
  10. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/jm_option.py +31 -5
  11. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/jm_plugin.py +5 -5
  12. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/jm_toolkit.py +31 -21
  13. {jmcomic-2.5.5 → jmcomic-2.5.7/src/jmcomic.egg-info}/PKG-INFO +31 -18
  14. {jmcomic-2.5.5 → jmcomic-2.5.7}/LICENSE +0 -0
  15. {jmcomic-2.5.5 → jmcomic-2.5.7}/setup.cfg +0 -0
  16. {jmcomic-2.5.5 → jmcomic-2.5.7}/setup.py +0 -0
  17. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/cl.py +0 -0
  18. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic/jm_exception.py +0 -0
  19. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic.egg-info/SOURCES.txt +0 -0
  20. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic.egg-info/dependency_links.txt +0 -0
  21. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic.egg-info/entry_points.txt +0 -0
  22. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic.egg-info/requires.txt +0 -0
  23. {jmcomic-2.5.5 → jmcomic-2.5.7}/src/jmcomic.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jmcomic
3
- Version: 2.5.5
3
+ Version: 2.5.7
4
4
  Summary: Python API For JMComic (禁漫天堂)
5
5
  Home-page: https://github.com/hect0x7/JMComic-Crawler-Python
6
6
  Author: hect0x7
@@ -42,16 +42,26 @@ Requires-Dist: pycryptodome
42
42
 
43
43
  本项目的核心功能是下载本子,基于此,设计了一套方便使用、便于扩展,能满足一些特殊下载需求的框架。
44
44
 
45
- 除了下载功能以外,也实现了其他的一些禁漫接口,例如登录、搜索、收藏夹、分类、排行榜等等,按需实现。
45
+ 目前核心功能实现较为稳定,项目也处于维护阶段。
46
46
 
47
- 目前核心功能实现较为稳定,项目也处于维护阶段(因为禁漫接口经常变动,需要经常维护)。
47
+ 除了下载功能以外,也实现了其他的一些禁漫接口,按需实现,具体如下。
48
+
49
+ ### 已实现的禁漫API:
50
+
51
+ - 登录
52
+ - 搜本
53
+ - 分类 (排行榜)
54
+ - 本子章节详情
55
+ - 图片下载解码
56
+ - 收藏夹
57
+ - 移动端接口加解密
48
58
 
49
59
  ## 安装教程
50
60
 
51
61
  * 通过pip官方源安装(推荐,并且更新也是这个命令)
52
62
 
53
63
  ```shell
54
- pip install jmcomic -i https://pypi.org/project --upgrade
64
+ pip install jmcomic -i https://pypi.org/project -U
55
65
  ```
56
66
  * 通过源代码安装
57
67
 
@@ -75,6 +85,21 @@ jmcomic.download_album('422866') # 传入要下载的album的id,即可下载
75
85
  $ jmcomic 422866
76
86
  ```
77
87
 
88
+ ## 进阶使用
89
+
90
+ 文档网站:[jmcomic.readthedocs.io](https://jmcomic.readthedocs.io/en/latest)
91
+
92
+ 进阶使用可以参考:[jmcomic常用类和方法演示](assets/docs/sources/tutorial/0_demo.md)
93
+
94
+ 下面列出的是一些常用的文档:
95
+
96
+ * [jmcomic常用类和方法演示](assets/docs/sources/tutorial/0_demo.md)
97
+ * [option配置文件语法(包含插件配置)](./assets/docs/sources/option_file_syntax.md)
98
+ * [GitHub Actions使用教程](./assets/docs/sources/tutorial/1_github_actions.md)
99
+ * [命令行使用教程](assets/docs/sources/tutorial/2_command_line.md)
100
+ * [插件机制](assets/docs/sources/tutorial/6_plugin.md)
101
+ * [下载过滤器机制](assets/docs/sources/tutorial/5_filter.md)
102
+
78
103
  ## 项目特点
79
104
 
80
105
  - **绕过Cloudflare的反爬虫**
@@ -111,19 +136,6 @@ $ jmcomic 422866
111
136
  - `jpg图片合成为一个pdf插件`
112
137
  - `导出收藏夹为csv文件插件`
113
138
 
114
- ## 进阶使用
115
-
116
- 进阶使用请查阅文档:[文档](https://jmcomic.readthedocs.io/en/latest)
117
-
118
- 下面列出一些常用的文档链接:
119
-
120
- * [option配置文件语法(包含插件配置)](./assets/docs/sources/option_file_syntax.md)
121
- * [常用类和方法演示(下载本子、获取实体类、搜索本子)](assets/docs/sources/tutorial/3_demo.md)
122
- * [命令行使用教程](assets/docs/sources/tutorial/2_command_line.md)
123
- * [GitHub Actions使用教程](./assets/docs/sources/tutorial/1_github_actions.md)
124
- * [插件机制](assets/docs/sources/tutorial/6_plugin.md)
125
- * [下载过滤器机制](assets/docs/sources/tutorial/5_filter.md)
126
-
127
139
  ## 使用小说明
128
140
 
129
141
  * Python >= 3.7
@@ -131,10 +143,11 @@ $ jmcomic 422866
131
143
 
132
144
  ## 项目文件夹介绍
133
145
 
146
+ * .github:GitHub Actions配置文件
134
147
  * assets:存放一些非代码的资源文件
135
148
 
136
- * config:存放配置文件
137
149
  * docs:项目文档
150
+ * option:存放配置文件
138
151
 
139
152
  * src:存放源代码
140
153
 
@@ -14,16 +14,26 @@
14
14
 
15
15
  本项目的核心功能是下载本子,基于此,设计了一套方便使用、便于扩展,能满足一些特殊下载需求的框架。
16
16
 
17
- 除了下载功能以外,也实现了其他的一些禁漫接口,例如登录、搜索、收藏夹、分类、排行榜等等,按需实现。
17
+ 目前核心功能实现较为稳定,项目也处于维护阶段。
18
18
 
19
- 目前核心功能实现较为稳定,项目也处于维护阶段(因为禁漫接口经常变动,需要经常维护)。
19
+ 除了下载功能以外,也实现了其他的一些禁漫接口,按需实现,具体如下。
20
+
21
+ ### 已实现的禁漫API:
22
+
23
+ - 登录
24
+ - 搜本
25
+ - 分类 (排行榜)
26
+ - 本子章节详情
27
+ - 图片下载解码
28
+ - 收藏夹
29
+ - 移动端接口加解密
20
30
 
21
31
  ## 安装教程
22
32
 
23
33
  * 通过pip官方源安装(推荐,并且更新也是这个命令)
24
34
 
25
35
  ```shell
26
- pip install jmcomic -i https://pypi.org/project --upgrade
36
+ pip install jmcomic -i https://pypi.org/project -U
27
37
  ```
28
38
  * 通过源代码安装
29
39
 
@@ -47,6 +57,21 @@ jmcomic.download_album('422866') # 传入要下载的album的id,即可下载
47
57
  $ jmcomic 422866
48
58
  ```
49
59
 
60
+ ## 进阶使用
61
+
62
+ 文档网站:[jmcomic.readthedocs.io](https://jmcomic.readthedocs.io/en/latest)
63
+
64
+ 进阶使用可以参考:[jmcomic常用类和方法演示](assets/docs/sources/tutorial/0_demo.md)
65
+
66
+ 下面列出的是一些常用的文档:
67
+
68
+ * [jmcomic常用类和方法演示](assets/docs/sources/tutorial/0_demo.md)
69
+ * [option配置文件语法(包含插件配置)](./assets/docs/sources/option_file_syntax.md)
70
+ * [GitHub Actions使用教程](./assets/docs/sources/tutorial/1_github_actions.md)
71
+ * [命令行使用教程](assets/docs/sources/tutorial/2_command_line.md)
72
+ * [插件机制](assets/docs/sources/tutorial/6_plugin.md)
73
+ * [下载过滤器机制](assets/docs/sources/tutorial/5_filter.md)
74
+
50
75
  ## 项目特点
51
76
 
52
77
  - **绕过Cloudflare的反爬虫**
@@ -83,19 +108,6 @@ $ jmcomic 422866
83
108
  - `jpg图片合成为一个pdf插件`
84
109
  - `导出收藏夹为csv文件插件`
85
110
 
86
- ## 进阶使用
87
-
88
- 进阶使用请查阅文档:[文档](https://jmcomic.readthedocs.io/en/latest)
89
-
90
- 下面列出一些常用的文档链接:
91
-
92
- * [option配置文件语法(包含插件配置)](./assets/docs/sources/option_file_syntax.md)
93
- * [常用类和方法演示(下载本子、获取实体类、搜索本子)](assets/docs/sources/tutorial/3_demo.md)
94
- * [命令行使用教程](assets/docs/sources/tutorial/2_command_line.md)
95
- * [GitHub Actions使用教程](./assets/docs/sources/tutorial/1_github_actions.md)
96
- * [插件机制](assets/docs/sources/tutorial/6_plugin.md)
97
- * [下载过滤器机制](assets/docs/sources/tutorial/5_filter.md)
98
-
99
111
  ## 使用小说明
100
112
 
101
113
  * Python >= 3.7
@@ -103,10 +115,11 @@ $ jmcomic 422866
103
115
 
104
116
  ## 项目文件夹介绍
105
117
 
118
+ * .github:GitHub Actions配置文件
106
119
  * assets:存放一些非代码的资源文件
107
120
 
108
- * config:存放配置文件
109
121
  * docs:项目文档
122
+ * option:存放配置文件
110
123
 
111
124
  * src:存放源代码
112
125
 
@@ -2,7 +2,7 @@
2
2
  # 被依赖方 <--- 使用方
3
3
  # config <--- entity <--- toolkit <--- client <--- option <--- downloader
4
4
 
5
- __version__ = '2.5.5'
5
+ __version__ = '2.5.7'
6
6
 
7
7
  from .api import *
8
8
  from .jm_plugin import *
@@ -1,11 +1,12 @@
1
1
  from .jm_downloader import *
2
2
 
3
+ __DOWNLOAD_API_RET = Tuple[JmAlbumDetail, JmDownloader]
3
4
 
4
5
  def download_batch(download_api,
5
6
  jm_id_iter: Union[Iterable, Generator],
6
7
  option=None,
7
8
  downloader=None,
8
- ) -> Set[Tuple[JmAlbumDetail, JmDownloader]]:
9
+ ) -> Set[__DOWNLOAD_API_RET]:
9
10
  """
10
11
  批量下载 album / photo
11
12
 
@@ -46,7 +47,7 @@ def download_album(jm_album_id,
46
47
  option=None,
47
48
  downloader=None,
48
49
  callback=None,
49
- ):
50
+ ) -> Union[__DOWNLOAD_API_RET, Set[__DOWNLOAD_API_RET]]:
50
51
  """
51
52
  下载一个本子(album),包含其所有的章节(photo)
52
53
 
@@ -100,7 +101,7 @@ def new_downloader(option=None, downloader=None) -> JmDownloader:
100
101
  return downloader(option)
101
102
 
102
103
 
103
- def create_option(filepath):
104
+ def create_option_by_file(filepath):
104
105
  return JmModuleConfig.option_class().from_file(filepath)
105
106
 
106
107
 
@@ -110,4 +111,14 @@ def create_option_by_env(env_name='JM_OPTION_PATH'):
110
111
  filepath = get_env(env_name, None)
111
112
  ExceptionTool.require_true(filepath is not None,
112
113
  f'未配置环境变量: {env_name},请配置为option的文件路径')
113
- return create_option(filepath)
114
+ return create_option_by_file(filepath)
115
+
116
+
117
+ def create_option_by_str(text: str, mode=None):
118
+ if mode is None:
119
+ mode = PackerUtil.mode_yml
120
+ data = PackerUtil.unpack_by_str(text, mode)[0]
121
+ return JmModuleConfig.option_class().construct(data)
122
+
123
+
124
+ create_option = create_option_by_file
@@ -79,9 +79,9 @@ class AbstractJmClient(
79
79
  """
80
80
  if domain_index >= len(self.domain_list):
81
81
  return self.fallback(request, url, domain_index, retry_count, **kwargs)
82
-
82
+
83
83
  url_backup = url
84
-
84
+
85
85
  if url.startswith('/'):
86
86
  # path → url
87
87
  domain = self.domain_list[domain_index]
@@ -976,10 +976,14 @@ class JmApiClient(AbstractJmClient):
976
976
  return cookies
977
977
 
978
978
 
979
- class FutureClientProxy(JmcomicClient):
979
+ class PhotoConcurrentFetcherProxy(JmcomicClient):
980
980
  """
981
- 在Client上做了一层线程池封装来实现异步,对外仍然暴露JmcomicClient的接口,可以看作Client的代理。
982
- 除了使用线程池做异步,还通过加锁和缓存结果,实现同一个请求不会被多个线程发出,减少开销
981
+ 为了解决 JmApiClient.get_photo_detail 方法的排队调用问题,
982
+ 即在访问完photo的接口后,需要另外排队访问获取album和scramble_id的接口。
983
+
984
+ 这三个接口可以并发请求,这样可以提高效率。
985
+
986
+ 此Proxy代理了get_photo_detail,实现了并发请求这三个接口,然后组装返回值返回photo。
983
987
 
984
988
  可通过插件 ClientProxyPlugin 启用本类,配置如下:
985
989
  ```yml
@@ -987,10 +991,10 @@ class FutureClientProxy(JmcomicClient):
987
991
  after_init:
988
992
  - plugin: client_proxy
989
993
  kwargs:
990
- proxy_client_key: cl_proxy_future
994
+ proxy_client_key: photo_concurrent_fetcher_proxy
991
995
  ```
992
996
  """
993
- client_key = 'cl_proxy_future'
997
+ client_key = 'photo_concurrent_fetcher_proxy'
994
998
 
995
999
  class FutureWrapper:
996
1000
  def __init__(self, future, after_done_callback):
@@ -1024,16 +1028,15 @@ class FutureClientProxy(JmcomicClient):
1024
1028
  executors = ThreadPoolExecutor(max_workers)
1025
1029
 
1026
1030
  self.executors = executors
1027
- self.future_dict: Dict[str, FutureClientProxy.FutureWrapper] = {}
1031
+ self.future_dict: Dict[str, PhotoConcurrentFetcherProxy.FutureWrapper] = {}
1028
1032
  from threading import Lock
1029
1033
  self.lock = Lock()
1030
1034
 
1031
1035
  def route_notimpl_method_to_internal_client(self, client):
1032
1036
 
1033
- impl_methods = str_to_set('''
1037
+ proxy_methods = str_to_set('''
1034
1038
  get_album_detail
1035
1039
  get_photo_detail
1036
- search
1037
1040
  ''')
1038
1041
 
1039
1042
  # 获取对象的所有属性和方法的名称列表
@@ -1043,7 +1046,7 @@ class FutureClientProxy(JmcomicClient):
1043
1046
  # 判断是否为方法(可调用对象)
1044
1047
  if (not method.startswith('_')
1045
1048
  and callable(getattr(client, method))
1046
- and method not in impl_methods
1049
+ and method not in proxy_methods
1047
1050
  ):
1048
1051
  setattr(self, method, getattr(client, method))
1049
1052
 
@@ -1055,15 +1058,19 @@ class FutureClientProxy(JmcomicClient):
1055
1058
 
1056
1059
  def get_future(self, cache_key, task):
1057
1060
  if cache_key in self.future_dict:
1061
+ # cache hit, means that a same task is running
1058
1062
  return self.future_dict[cache_key]
1059
1063
 
1060
1064
  with self.lock:
1061
1065
  if cache_key in self.future_dict:
1062
1066
  return self.future_dict[cache_key]
1063
1067
 
1068
+ # after future done, remove it from future_dict.
1069
+ # cache depends on self.client instead of self.future_dict
1064
1070
  future = self.FutureWrapper(self.executors.submit(task),
1065
1071
  after_done_callback=lambda: self.future_dict.pop(cache_key, None)
1066
1072
  )
1073
+
1067
1074
  self.future_dict[cache_key] = future
1068
1075
  return future
1069
1076
 
@@ -1115,8 +1122,3 @@ class FutureClientProxy(JmcomicClient):
1115
1122
  photo.scramble_id = scramble_id
1116
1123
 
1117
1124
  return photo
1118
-
1119
- def search(self, search_query: str, page: int, main_tag: int, order_by: str, time: str) -> JmSearchPage:
1120
- cache_key = f'search_query_{search_query}_page_{page}_main_tag_{main_tag}_order_by_{order_by}_time_{time}'
1121
- future = self.get_future(cache_key, task=lambda: self.client.search(search_query, page, main_tag, order_by, time))
1122
- return future.result()
@@ -6,8 +6,6 @@ Response Entity
6
6
 
7
7
  """
8
8
 
9
- DictModel = AdvancedEasyAccessDict
10
-
11
9
 
12
10
  class JmResp:
13
11
 
@@ -87,11 +85,11 @@ class JmJsonResp(JmResp):
87
85
  def json(self) -> Dict:
88
86
  try:
89
87
  return self.resp.json()
90
- except Exception:
91
- ExceptionTool.raises_resp('json解析失败', self, JsonResolveFailException)
88
+ except Exception as e:
89
+ ExceptionTool.raises_resp(f'json解析失败: {e}', self, JsonResolveFailException)
92
90
 
93
- def model(self) -> DictModel:
94
- return DictModel(self.json())
91
+ def model(self) -> AdvancedEasyAccessDict:
92
+ return AdvancedEasyAccessDict(self.json())
95
93
 
96
94
 
97
95
  class JmApiResp(JmJsonResp):
@@ -120,9 +118,9 @@ class JmApiResp(JmJsonResp):
120
118
  return loads(self.decoded_data)
121
119
 
122
120
  @property
123
- def model_data(self) -> DictModel:
121
+ def model_data(self) -> AdvancedEasyAccessDict:
124
122
  self.require_success()
125
- return DictModel(self.res_data)
123
+ return AdvancedEasyAccessDict(self.res_data)
126
124
 
127
125
 
128
126
  # album-comment
@@ -469,11 +467,14 @@ class JmcomicClient(
469
467
  def of_api_url(self, api_path, domain):
470
468
  raise NotImplementedError
471
469
 
472
- def get_html_domain(self, postman=None):
473
- return JmModuleConfig.get_html_domain(postman or self.get_root_postman())
470
+ def get_html_domain(self):
471
+ return JmModuleConfig.get_html_domain(self.get_root_postman())
472
+
473
+ def get_html_domain_all(self):
474
+ return JmModuleConfig.get_html_domain_all(self.get_root_postman())
474
475
 
475
- def get_html_domain_all(self, postman=None):
476
- return JmModuleConfig.get_html_domain_all(postman or self.get_root_postman())
476
+ def get_html_domain_all_via_github(self):
477
+ return JmModuleConfig.get_html_domain_all_via_github(self.get_root_postman())
477
478
 
478
479
  # noinspection PyMethodMayBeStatic
479
480
  def do_page_iter(self, params: dict, page: int, get_page_method):
@@ -2,8 +2,8 @@ from common import time_stamp, str_to_list, field_cache, ProxyBuilder
2
2
 
3
3
 
4
4
  def default_jm_logging(topic: str, msg: str):
5
- from common import format_ts
6
- print(f'{format_ts()}:【{topic}】{msg}')
5
+ from common import format_ts, current_thread
6
+ print('[{}] [{}]:【{}】{}'.format(format_ts(), current_thread().name, topic, msg))
7
7
 
8
8
 
9
9
  # 禁漫常量
@@ -24,17 +24,18 @@ class JmMagicConstants:
24
24
  TIME_MONTH = 'm'
25
25
  TIME_ALL = 'a'
26
26
 
27
- # 全部, 同人, 单本, 短篇, 其他, 韩漫, 美漫, cosplay, 3D
28
- # category = ["0", "doujin", "single", "short", "another", "hanman", "meiman", "doujin_cosplay", "3D"]
29
- CATEGORY_ALL = '0'
30
- CATEGORY_DOUJIN = 'doujin'
31
- CATEGORY_SINGLE = 'single'
32
- CATEGORY_SHORT = 'short'
33
- CATEGORY_ANOTHER = 'another'
34
- CATEGORY_HANMAN = 'hanman'
35
- CATEGORY_MEIMAN = 'meiman'
36
- CATEGORY_DOUJIN_COSPLAY = 'doujin_cosplay'
37
- CATEGORY_3D = '3D'
27
+ # 分类参数API接口的category
28
+ CATEGORY_ALL = '0' # 全部
29
+ CATEGORY_DOUJIN = 'doujin' # 同人
30
+ CATEGORY_SINGLE = 'single' # 单本
31
+ CATEGORY_SHORT = 'short' # 短篇
32
+ CATEGORY_ANOTHER = 'another' # 其他
33
+ CATEGORY_HANMAN = 'hanman' # 韩漫
34
+ CATEGORY_MEIMAN = 'meiman' # 美漫
35
+ CATEGORY_DOUJIN_COSPLAY = 'doujin_cosplay' # cosplay
36
+ CATEGORY_3D = '3D' # 3D
37
+ CATEGORY_ENGLISH_SITE = 'english_site' # 英文站
38
+ CATEGORY_JM_TEAM = '禁漫漢化組'
38
39
 
39
40
  # 分页大小
40
41
  PAGE_SIZE_SEARCH = 80
@@ -52,10 +53,10 @@ class JmMagicConstants:
52
53
  APP_TOKEN_SECRET = '18comicAPP'
53
54
  APP_TOKEN_SECRET_2 = '18comicAPPContent'
54
55
  APP_DATA_SECRET = '185Hcomic3PAPP7R'
55
- APP_VERSION = '1.6.6'
56
+ APP_VERSION = '1.6.7'
56
57
  APP_HEADERS_TEMPLATE = {
57
58
  'Accept-Encoding': 'gzip',
58
- 'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.09211555; wv) AppleWebKit/537.36 (KHTML, '
59
+ 'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.11211812; wv) AppleWebKit/537.36 (KHTML, '
59
60
  'like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36',
60
61
  }
61
62
 
@@ -81,7 +82,7 @@ class JmModuleConfig:
81
82
  # 网站相关
82
83
  PROT = "https://"
83
84
  JM_REDIRECT_URL = f'{PROT}jm365.work/3YeBdF' # 永久網域,怕走失的小伙伴收藏起来
84
- JM_PUB_URL = f'{PROT}jmcomic.ltd'
85
+ JM_PUB_URL = f'{PROT}jmcomic-fb.vip'
85
86
  JM_CDN_IMAGE_URL_TEMPLATE = PROT + 'cdn-msp.{domain}/media/photos/{photo_id}/{index:05}{suffix}' # index 从1开始
86
87
  JM_IMAGE_SUFFIX = ['.jpg', '.webp', '.png', '.gif']
87
88
 
@@ -259,6 +260,41 @@ class JmModuleConfig:
259
260
  cls.jm_log('module.html_domain_all', f'获取禁漫网页全部域名: [{resp.url}] → {domain_list}')
260
261
  return domain_list
261
262
 
263
+ @classmethod
264
+ def get_html_domain_all_via_github(cls,
265
+ postman=None,
266
+ template='https://jmcmomic.github.io/go/{}.html',
267
+ index_range=(300, 309)
268
+ ):
269
+ """
270
+ 通过禁漫官方的github号的repo获取最新的禁漫域名
271
+ https://github.com/jmcmomic/jmcmomic.github.io
272
+ """
273
+ postman = postman or cls.new_postman(headers={
274
+ 'authority': 'github.com',
275
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 '
276
+ 'Safari/537.36'
277
+ })
278
+ domain_set = set()
279
+
280
+ def fetch_domain(url):
281
+ resp = postman.get(url, allow_redirects=False)
282
+ text = resp.text
283
+ from .jm_toolkit import JmcomicText
284
+ for domain in JmcomicText.analyse_jm_pub_html(text):
285
+ if domain.startswith('jm365'):
286
+ continue
287
+ domain_set.add(domain)
288
+
289
+ from common import multi_thread_launcher
290
+
291
+ multi_thread_launcher(
292
+ iter_objs=[template.format(i) for i in range(*index_range)],
293
+ apply_each_obj_func=fetch_domain,
294
+ )
295
+
296
+ return domain_set
297
+
262
298
  @classmethod
263
299
  def new_html_headers(cls, domain='18comic.vip'):
264
300
  """
@@ -50,8 +50,10 @@ class JmDownloader(DownloadCallback):
50
50
 
51
51
  def __init__(self, option: JmOption) -> None:
52
52
  self.option = option
53
- # 收集所有下载的image,为plugin提供数据
54
- self.all_downloaded: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {}
53
+ # 下载成功的记录dict
54
+ self.download_success_dict: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {}
55
+ # 下载失败的记录list
56
+ self.download_failed_list: List[Tuple[JmImageDetail, BaseException]] = []
55
57
 
56
58
  def download_album(self, album_id):
57
59
  client = self.client_for_album(album_id)
@@ -101,11 +103,21 @@ class JmDownloader(DownloadCallback):
101
103
  if use_cache is True and image.is_exists:
102
104
  return
103
105
 
104
- client.download_by_image_detail(
105
- image,
106
- img_save_path,
107
- decode_image=decode_image,
108
- )
106
+ e = None
107
+ try:
108
+ client.download_by_image_detail(
109
+ image,
110
+ img_save_path,
111
+ decode_image=decode_image,
112
+ )
113
+ except BaseException as e:
114
+ jm_log('image.failed', f'图片下载失败: [{image.download_url}], 异常: {e}')
115
+ # 保存失败记录
116
+ self.download_failed_list.append((image, e))
117
+
118
+ if e is not None:
119
+ raise e
120
+
109
121
  self.after_image(image, img_save_path)
110
122
 
111
123
  # noinspection PyMethodMayBeStatic
@@ -164,16 +176,38 @@ class JmDownloader(DownloadCallback):
164
176
  """
165
177
  return self.option.build_jm_client()
166
178
 
179
+ @property
180
+ def all_success(self) -> bool:
181
+ """
182
+ 是否成功下载了全部图片
183
+
184
+ 该属性需要等到downloader的全部download_xxx方法完成后才有意义。
185
+
186
+ 注意!如果使用了filter机制,例如通过filter只下载3张图片,那么all_success也会为False
187
+ """
188
+ if len(self.download_failed_list) != 0:
189
+ return False
190
+
191
+ for album, photo_dict in self.download_success_dict.items():
192
+ if len(album) != len(photo_dict):
193
+ return False
194
+
195
+ for photo, image_list in photo_dict.items():
196
+ if len(photo) != len(image_list):
197
+ return False
198
+
199
+ return True
200
+
167
201
  # 下面是回调方法
168
202
 
169
203
  def before_album(self, album: JmAlbumDetail):
170
204
  super().before_album(album)
171
- self.all_downloaded.setdefault(album, {})
172
-
173
- def before_photo(self, photo: JmPhotoDetail):
174
- super().before_photo(photo)
175
- self.all_downloaded.setdefault(photo.from_album, {})
176
- self.all_downloaded[photo.from_album].setdefault(photo, [])
205
+ self.download_success_dict.setdefault(album, {})
206
+ self.option.call_all_plugin(
207
+ 'before_album',
208
+ album=album,
209
+ downloader=self,
210
+ )
177
211
 
178
212
  def after_album(self, album: JmAlbumDetail):
179
213
  super().after_album(album)
@@ -183,6 +217,16 @@ class JmDownloader(DownloadCallback):
183
217
  downloader=self,
184
218
  )
185
219
 
220
+ def before_photo(self, photo: JmPhotoDetail):
221
+ super().before_photo(photo)
222
+ self.download_success_dict.setdefault(photo.from_album, {})
223
+ self.download_success_dict[photo.from_album].setdefault(photo, [])
224
+ self.option.call_all_plugin(
225
+ 'before_photo',
226
+ photo=photo,
227
+ downloader=self,
228
+ )
229
+
186
230
  def after_photo(self, photo: JmPhotoDetail):
187
231
  super().after_photo(photo)
188
232
  self.option.call_all_plugin(
@@ -191,12 +235,25 @@ class JmDownloader(DownloadCallback):
191
235
  downloader=self,
192
236
  )
193
237
 
238
+ def before_image(self, image: JmImageDetail, img_save_path):
239
+ super().before_image(image, img_save_path)
240
+ self.option.call_all_plugin(
241
+ 'before_image',
242
+ image=image,
243
+ downloader=self,
244
+ )
245
+
194
246
  def after_image(self, image: JmImageDetail, img_save_path):
195
247
  super().after_image(image, img_save_path)
196
248
  photo = image.from_photo
197
249
  album = photo.from_album
198
250
 
199
- self.all_downloaded.get(album).get(photo).append((img_save_path, image))
251
+ self.download_success_dict.get(album).get(photo).append((img_save_path, image))
252
+ self.option.call_all_plugin(
253
+ 'after_image',
254
+ image=image,
255
+ downloader=self,
256
+ )
200
257
 
201
258
  # 下面是对with语法的支持
202
259
 
@@ -219,28 +276,23 @@ class JmDownloader(DownloadCallback):
219
276
 
220
277
  class DoNotDownloadImage(JmDownloader):
221
278
  """
222
- 本类仅用于测试
223
-
224
- 用法:
225
-
226
- JmModuleConfig.CLASS_DOWNLOADER = DoNotDownloadImage
279
+ 不会下载任何图片的Downloader,用作测试
227
280
  """
228
281
 
229
282
  def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
230
283
  # ensure make dir
231
284
  self.option.decide_image_filepath(image)
232
- pass
233
285
 
234
286
 
235
287
  class JustDownloadSpecificCountImage(JmDownloader):
288
+ """
289
+ 只下载特定数量图片的Downloader,用作测试
290
+ """
236
291
  from threading import Lock
237
292
 
238
293
  count_lock = Lock()
239
294
  count = 0
240
295
 
241
- def __init__(self, option: JmOption) -> None:
242
- super().__init__(option)
243
-
244
296
  def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
245
297
  # ensure make dir
246
298
  self.option.decide_image_filepath(image)
@@ -91,17 +91,17 @@ class DetailEntity(JmBaseEntity, IndexedEntity):
91
91
  """
92
92
  authoroname = author + oname
93
93
 
94
- 比较好识别的一种本子名称方式
94
+ 个人认为识别度比较高的本子名称,一眼看去就能获取到本子的关键信息
95
95
 
96
- 具体格式: f'【author】{oname}'
96
+ 具体格式: '【author】oname'
97
97
 
98
98
  示例:
99
99
 
100
- 原本子名:喂我吃吧 老師! [欶瀾漢化組] [BLVEFO9] たべさせて、せんせい! (ブルーアーカイブ) [中國翻譯] [無修正]
100
+ Pname:喂我吃吧 老師! [欶瀾漢化組] [BLVEFO9] たべさせて、せんせい! (ブルーアーカイブ) [中國翻譯] [無修正]
101
101
 
102
- authoroname:【BLVEFO9】喂我吃吧 老師!
102
+ Pauthoroname:【BLVEFO9】喂我吃吧 老師!
103
103
 
104
- :return: 返回作者名+作品原名,格式为: '【author】{oname}'
104
+ :return: 返回作者名+本子原始名称,格式为: '【author】oname'
105
105
  """
106
106
  return f'【{self.author}】{self.oname}'
107
107
 
@@ -109,12 +109,16 @@ class DetailEntity(JmBaseEntity, IndexedEntity):
109
109
  def idoname(self):
110
110
  """
111
111
  类似 authoroname
112
- :return: '[id] {oname}'
112
+
113
+ :return: '[id] oname'
113
114
  """
114
115
  return f'[{self.id}] {self.oname}'
115
116
 
116
117
  def __str__(self):
117
- return f'{self.__class__.__name__}({self.id}-{self.title})'
118
+ return f'{self.__class__.__name__}' \
119
+ '{' \
120
+ f'{self.id}: {self.title}'\
121
+ '}'
118
122
 
119
123
  @classmethod
120
124
  def __alias__(cls):
@@ -105,13 +105,11 @@ class DirRule:
105
105
  解析下载路径dsl,得到一个路径规则解析列表
106
106
  """
107
107
 
108
- if '_' not in rule_dsl and rule_dsl != 'Bd':
109
- ExceptionTool.raises(f'不支持的dsl: "{rule_dsl}"')
110
-
111
- rule_list = rule_dsl.split('_')
108
+ rule_list = self.split_rule_dsl(rule_dsl)
112
109
  solver_ls: List[DirRule.RuleSolver] = []
113
110
 
114
111
  for rule in rule_list:
112
+ rule = rule.strip()
115
113
  if rule == 'Bd':
116
114
  solver_ls.append((0, lambda _: base_dir, 'Bd'))
117
115
  continue
@@ -124,6 +122,19 @@ class DirRule:
124
122
 
125
123
  return solver_ls
126
124
 
125
+ # noinspection PyMethodMayBeStatic
126
+ def split_rule_dsl(self, rule_dsl: str) -> List[str]:
127
+ if rule_dsl == 'Bd':
128
+ return [rule_dsl]
129
+
130
+ if '/' in rule_dsl:
131
+ return rule_dsl.split('/')
132
+
133
+ if '_' in rule_dsl:
134
+ return rule_dsl.split('_')
135
+
136
+ ExceptionTool.raises(f'不支持的rule配置: "{rule_dsl}"')
137
+
127
138
  @classmethod
128
139
  def get_rule_solver(cls, rule: str) -> Optional[RuleSolver]:
129
140
  # 查找缓存
@@ -181,6 +192,7 @@ class JmOption:
181
192
  client: Dict,
182
193
  plugins: Dict,
183
194
  filepath=None,
195
+ call_after_init_plugin=True,
184
196
  ):
185
197
  # 路径规则配置
186
198
  self.dir_rule = DirRule(**dir_rule)
@@ -196,7 +208,21 @@ class JmOption:
196
208
  # 需要主线程等待完成的插件
197
209
  self.need_wait_plugins = []
198
210
 
199
- self.call_all_plugin('after_init', safe=True)
211
+ if call_after_init_plugin:
212
+ self.call_all_plugin('after_init', safe=True)
213
+
214
+ def copy_option(self):
215
+ return self.__class__(
216
+ dir_rule={
217
+ 'rule': self.dir_rule.rule_dsl,
218
+ 'base_dir': self.dir_rule.base_dir,
219
+ },
220
+ download=self.download.src_dict,
221
+ client=self.client.src_dict,
222
+ plugins=self.plugins.src_dict,
223
+ filepath=self.filepath,
224
+ call_after_init_plugin=False
225
+ )
200
226
 
201
227
  """
202
228
  下面是decide系列方法,为了支持重写和增加程序动态性。
@@ -298,7 +298,7 @@ class ZipPlugin(JmOptionPlugin):
298
298
 
299
299
  # 原文件夹 -> zip文件
300
300
  dir_zip_dict: Dict[str, Optional[str]] = {}
301
- photo_dict = downloader.all_downloaded[album]
301
+ photo_dict = downloader.download_success_dict[album]
302
302
 
303
303
  if level == 'album':
304
304
  zip_path = self.get_zip_path(album, None, filename_rule, suffix, zip_dir)
@@ -383,7 +383,7 @@ class ZipPlugin(JmOptionPlugin):
383
383
  dirs = sorted(dir_zip_dict.keys(), reverse=True)
384
384
  image_paths = [
385
385
  path
386
- for photo_dict in self.downloader.all_downloaded.values()
386
+ for photo_dict in self.downloader.download_success_dict.values()
387
387
  for image_list in photo_dict.values()
388
388
  for path, image in image_list
389
389
  ]
@@ -752,7 +752,7 @@ class ConvertJpgToPdfPlugin(JmOptionPlugin):
752
752
 
753
753
  paths = [
754
754
  path
755
- for path, image in downloader.all_downloaded[photo.from_album][photo]
755
+ for path, image in downloader.download_success_dict[photo.from_album][photo]
756
756
  ]
757
757
 
758
758
  paths.append(self.option.decide_image_save_dir(photo, ensure_exists=False))
@@ -814,8 +814,6 @@ class JmServerPlugin(JmOptionPlugin):
814
814
  if self.running is True:
815
815
  return
816
816
 
817
- self.running = True
818
-
819
817
  # 服务器的代码位于一个独立库:plugin_jm_server,需要独立安装
820
818
  # 源代码仓库:https://github.com/hect0x7/plugin-jm-server
821
819
  try:
@@ -842,6 +840,7 @@ class JmServerPlugin(JmOptionPlugin):
842
840
  # 不是主线程,return
843
841
  return self.warning_wrong_usage_of_debug()
844
842
  else:
843
+ self.running = True
845
844
  # 是主线程,启动服务器
846
845
  blocking_run_server()
847
846
 
@@ -849,6 +848,7 @@ class JmServerPlugin(JmOptionPlugin):
849
848
  # 非debug模式,开新线程启动
850
849
  threading.Thread(target=blocking_run_server, daemon=True).start()
851
850
  atexit_register(self.wait_server_stop)
851
+ self.running = True
852
852
 
853
853
  def warning_wrong_usage_of_debug(self):
854
854
  self.log('注意!当配置debug=True时,请确保当前插件是在主线程中被调用。\n'
@@ -357,17 +357,16 @@ class JmPageTool:
357
357
  # 用来缩减html的长度
358
358
  pattern_html_search_shorten_for = compile(r'<div class="well well-sm">([\s\S]*)<div class="row">')
359
359
 
360
- # 用来提取搜索页面的的album的信息
360
+ # 用来提取搜索页面的album的信息
361
361
  pattern_html_search_album_info_list = compile(
362
362
  r'<a href="/album/(\d+)/[\s\S]*?title="(.*?)"([\s\S]*?)<div class="title-truncate tags .*>([\s\S]*?)</div>'
363
363
  )
364
364
 
365
- # 用来提取分类页面的的album的信息
365
+ # 用来提取分类页面的album的信息
366
366
  pattern_html_category_album_info_list = compile(
367
- r'<a href="/album/(\d+)/[^>]*>[\s\S]*?title="(.*?)"[^>]*>'
368
- r'\n</a>\n'
369
- r'<div class="label-loveicon">'
370
- r'([\s\S]*?)'
367
+ r'<a href="/album/(\d+)/[^>]*>[^>]*?'
368
+ r'title="(.*?)"[^>]*>[ \n]*</a>[ \n]*'
369
+ r'<div class="label-loveicon">([\s\S]*?)'
371
370
  r'<div class="clearfix">'
372
371
  )
373
372
 
@@ -473,7 +472,7 @@ class JmPageTool:
473
472
  return JmFavoritePage(content, folder_list, total)
474
473
 
475
474
  @classmethod
476
- def parse_api_to_search_page(cls, data: DictModel) -> JmSearchPage:
475
+ def parse_api_to_search_page(cls, data: AdvancedEasyAccessDict) -> JmSearchPage:
477
476
  """
478
477
  model_data: {
479
478
  "search_query": "MANA",
@@ -502,7 +501,7 @@ class JmPageTool:
502
501
  return JmSearchPage(content, total)
503
502
 
504
503
  @classmethod
505
- def parse_api_to_favorite_page(cls, data: DictModel) -> JmFavoritePage:
504
+ def parse_api_to_favorite_page(cls, data: AdvancedEasyAccessDict) -> JmFavoritePage:
506
505
  """
507
506
  {
508
507
  "list": [
@@ -547,7 +546,7 @@ class JmPageTool:
547
546
 
548
547
  @classmethod
549
548
  def adapt_content(cls, content):
550
- def adapt_item(item: DictModel):
549
+ def adapt_item(item: AdvancedEasyAccessDict):
551
550
  item: dict = item.src_dict
552
551
  item.setdefault('tags', [])
553
552
  return item
@@ -674,7 +673,7 @@ class JmApiAdaptTool:
674
673
  series = data['series']
675
674
  episode_list = []
676
675
  for chapter in series:
677
- chapter = DictModel(chapter)
676
+ chapter = AdvancedEasyAccessDict(chapter)
678
677
  # photo_id, photo_index, photo_title, photo_pub_date
679
678
  episode_list.append(
680
679
  (chapter.id, chapter.sort, chapter.name, None)
@@ -689,7 +688,7 @@ class JmApiAdaptTool:
689
688
  sort = 1
690
689
  series: list = data['series'] # series中的sort从1开始
691
690
  for chapter in series:
692
- chapter = DictModel(chapter)
691
+ chapter = AdvancedEasyAccessDict(chapter)
693
692
  if int(chapter.id) == int(data['id']):
694
693
  sort = chapter.sort
695
694
  break
@@ -755,23 +754,34 @@ class JmImageTool:
755
754
 
756
755
  # 创建新的解密图片
757
756
  img_decode = Image.new("RGB", (w, h))
758
- remainder = h % num
759
- copyW = w
757
+ over = h % num
760
758
  for i in range(num):
761
- copyH = math.floor(h / num)
762
- py = copyH * i
763
- y = h - (copyH * (i + 1)) - remainder
759
+ move = math.floor(h / num)
760
+ y_src = h - (move * (i + 1)) - over
761
+ y_dst = move * i
764
762
 
765
763
  if i == 0:
766
- copyH += remainder
764
+ move += over
767
765
  else:
768
- py += remainder
766
+ y_dst += over
769
767
 
770
768
  img_decode.paste(
771
- img_src.crop((0, y, copyW, y + copyH)),
772
- (0, py, copyW, py + copyH)
769
+ img_src.crop((
770
+ 0, y_src,
771
+ w, y_src + move
772
+ )),
773
+ (
774
+ 0, y_dst,
775
+ w, y_dst + move
776
+ )
773
777
  )
774
778
 
779
+ # save every step result
780
+ # cls.save_image(img_decode, change_file_name(
781
+ # decoded_save_path,
782
+ # f'{of_file_name(decoded_save_path, trim_suffix=True)}_{i}{of_file_suffix(decoded_save_path)}'
783
+ # ))
784
+
775
785
  # 保存到新的解密文件
776
786
  cls.save_image(img_decode, decoded_save_path)
777
787
 
@@ -867,7 +877,7 @@ class JmCryptoTool:
867
877
  """
868
878
  解密接口返回值
869
879
 
870
- :param data: data = resp.json()['data]
880
+ :param data: resp.json()['data']
871
881
  :param ts: 时间戳
872
882
  :param secret: 密钥
873
883
  :return: json格式的字符串
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jmcomic
3
- Version: 2.5.5
3
+ Version: 2.5.7
4
4
  Summary: Python API For JMComic (禁漫天堂)
5
5
  Home-page: https://github.com/hect0x7/JMComic-Crawler-Python
6
6
  Author: hect0x7
@@ -42,16 +42,26 @@ Requires-Dist: pycryptodome
42
42
 
43
43
  本项目的核心功能是下载本子,基于此,设计了一套方便使用、便于扩展,能满足一些特殊下载需求的框架。
44
44
 
45
- 除了下载功能以外,也实现了其他的一些禁漫接口,例如登录、搜索、收藏夹、分类、排行榜等等,按需实现。
45
+ 目前核心功能实现较为稳定,项目也处于维护阶段。
46
46
 
47
- 目前核心功能实现较为稳定,项目也处于维护阶段(因为禁漫接口经常变动,需要经常维护)。
47
+ 除了下载功能以外,也实现了其他的一些禁漫接口,按需实现,具体如下。
48
+
49
+ ### 已实现的禁漫API:
50
+
51
+ - 登录
52
+ - 搜本
53
+ - 分类 (排行榜)
54
+ - 本子章节详情
55
+ - 图片下载解码
56
+ - 收藏夹
57
+ - 移动端接口加解密
48
58
 
49
59
  ## 安装教程
50
60
 
51
61
  * 通过pip官方源安装(推荐,并且更新也是这个命令)
52
62
 
53
63
  ```shell
54
- pip install jmcomic -i https://pypi.org/project --upgrade
64
+ pip install jmcomic -i https://pypi.org/project -U
55
65
  ```
56
66
  * 通过源代码安装
57
67
 
@@ -75,6 +85,21 @@ jmcomic.download_album('422866') # 传入要下载的album的id,即可下载
75
85
  $ jmcomic 422866
76
86
  ```
77
87
 
88
+ ## 进阶使用
89
+
90
+ 文档网站:[jmcomic.readthedocs.io](https://jmcomic.readthedocs.io/en/latest)
91
+
92
+ 进阶使用可以参考:[jmcomic常用类和方法演示](assets/docs/sources/tutorial/0_demo.md)
93
+
94
+ 下面列出的是一些常用的文档:
95
+
96
+ * [jmcomic常用类和方法演示](assets/docs/sources/tutorial/0_demo.md)
97
+ * [option配置文件语法(包含插件配置)](./assets/docs/sources/option_file_syntax.md)
98
+ * [GitHub Actions使用教程](./assets/docs/sources/tutorial/1_github_actions.md)
99
+ * [命令行使用教程](assets/docs/sources/tutorial/2_command_line.md)
100
+ * [插件机制](assets/docs/sources/tutorial/6_plugin.md)
101
+ * [下载过滤器机制](assets/docs/sources/tutorial/5_filter.md)
102
+
78
103
  ## 项目特点
79
104
 
80
105
  - **绕过Cloudflare的反爬虫**
@@ -111,19 +136,6 @@ $ jmcomic 422866
111
136
  - `jpg图片合成为一个pdf插件`
112
137
  - `导出收藏夹为csv文件插件`
113
138
 
114
- ## 进阶使用
115
-
116
- 进阶使用请查阅文档:[文档](https://jmcomic.readthedocs.io/en/latest)
117
-
118
- 下面列出一些常用的文档链接:
119
-
120
- * [option配置文件语法(包含插件配置)](./assets/docs/sources/option_file_syntax.md)
121
- * [常用类和方法演示(下载本子、获取实体类、搜索本子)](assets/docs/sources/tutorial/3_demo.md)
122
- * [命令行使用教程](assets/docs/sources/tutorial/2_command_line.md)
123
- * [GitHub Actions使用教程](./assets/docs/sources/tutorial/1_github_actions.md)
124
- * [插件机制](assets/docs/sources/tutorial/6_plugin.md)
125
- * [下载过滤器机制](assets/docs/sources/tutorial/5_filter.md)
126
-
127
139
  ## 使用小说明
128
140
 
129
141
  * Python >= 3.7
@@ -131,10 +143,11 @@ $ jmcomic 422866
131
143
 
132
144
  ## 项目文件夹介绍
133
145
 
146
+ * .github:GitHub Actions配置文件
134
147
  * assets:存放一些非代码的资源文件
135
148
 
136
- * config:存放配置文件
137
149
  * docs:项目文档
150
+ * option:存放配置文件
138
151
 
139
152
  * src:存放源代码
140
153
 
File without changes
File without changes
File without changes
File without changes