jmcomic 2.5.34__tar.gz → 2.5.36__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.34/src/jmcomic.egg-info → jmcomic-2.5.36}/PKG-INFO +1 -1
  2. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/__init__.py +1 -1
  3. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/api.py +9 -3
  4. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/jm_client_impl.py +1 -1
  5. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/jm_client_interface.py +4 -1
  6. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/jm_config.py +1 -1
  7. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/jm_downloader.py +74 -46
  8. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/jm_entity.py +34 -4
  9. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/jm_exception.py +9 -2
  10. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/jm_option.py +13 -5
  11. {jmcomic-2.5.34 → jmcomic-2.5.36/src/jmcomic.egg-info}/PKG-INFO +1 -1
  12. {jmcomic-2.5.34 → jmcomic-2.5.36}/LICENSE +0 -0
  13. {jmcomic-2.5.34 → jmcomic-2.5.36}/README.md +0 -0
  14. {jmcomic-2.5.34 → jmcomic-2.5.36}/setup.cfg +0 -0
  15. {jmcomic-2.5.34 → jmcomic-2.5.36}/setup.py +0 -0
  16. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/cl.py +0 -0
  17. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/jm_plugin.py +0 -0
  18. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic/jm_toolkit.py +0 -0
  19. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic.egg-info/SOURCES.txt +0 -0
  20. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic.egg-info/dependency_links.txt +0 -0
  21. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic.egg-info/entry_points.txt +0 -0
  22. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic.egg-info/requires.txt +0 -0
  23. {jmcomic-2.5.34 → jmcomic-2.5.36}/src/jmcomic.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmcomic
3
- Version: 2.5.34
3
+ Version: 2.5.36
4
4
  Summary: Python API For JMComic (禁漫天堂)
5
5
  Home-page: https://github.com/hect0x7/JMComic-Crawler-Python
6
6
  Author: hect0x7
@@ -2,7 +2,7 @@
2
2
  # 被依赖方 <--- 使用方
3
3
  # config <--- entity <--- toolkit <--- client <--- option <--- downloader
4
4
 
5
- __version__ = '2.5.34'
5
+ __version__ = '2.5.36'
6
6
 
7
7
  from .api import *
8
8
  from .jm_plugin import *
@@ -48,6 +48,7 @@ def download_album(jm_album_id,
48
48
  option=None,
49
49
  downloader=None,
50
50
  callback=None,
51
+ check_exception=True,
51
52
  ) -> Union[__DOWNLOAD_API_RET, Set[__DOWNLOAD_API_RET]]:
52
53
  """
53
54
  下载一个本子(album),包含其所有的章节(photo)
@@ -58,6 +59,7 @@ def download_album(jm_album_id,
58
59
  :param option: 下载选项
59
60
  :param downloader: 下载器类
60
61
  :param callback: 返回值回调函数,可以拿到 album 和 downloader
62
+ :param check_exception: 是否检查异常, 如果为True,会检查downloader是否有下载异常,并上抛PartialDownloadFailedException
61
63
  :return: 对于的本子实体类,下载器(如果是上述的批量情况,返回值为download_batch的返回值)
62
64
  """
63
65
 
@@ -69,14 +71,17 @@ def download_album(jm_album_id,
69
71
 
70
72
  if callback is not None:
71
73
  callback(album, dler)
72
-
74
+ if check_exception:
75
+ dler.raise_if_has_exception()
73
76
  return album, dler
74
77
 
75
78
 
76
79
  def download_photo(jm_photo_id,
77
80
  option=None,
78
81
  downloader=None,
79
- callback=None):
82
+ callback=None,
83
+ check_exception=True,
84
+ ):
80
85
  """
81
86
  下载一个章节(photo),参数同 download_album
82
87
  """
@@ -88,7 +93,8 @@ def download_photo(jm_photo_id,
88
93
 
89
94
  if callback is not None:
90
95
  callback(photo, dler)
91
-
96
+ if check_exception:
97
+ dler.raise_if_has_exception()
92
98
  return photo, dler
93
99
 
94
100
 
@@ -566,7 +566,7 @@ class JmHtmlClient(AbstractJmClient):
566
566
 
567
567
  cls.raise_request_error(
568
568
  resp,
569
- f'{reason}'
569
+ f'{reason}({content})'
570
570
  + (f': {url}' if url is not None else '')
571
571
  )
572
572
 
@@ -62,13 +62,16 @@ class JmImageResp(JmResp):
62
62
  img_url=None,
63
63
  ):
64
64
  img_url = img_url or self.url
65
+ index = img_url.find("?")
66
+ if index != -1:
67
+ img_url = img_url[0:index]
65
68
 
66
69
  if decode_image is False or scramble_id is None:
67
70
  # 不解密图片,直接保存文件
68
71
  JmImageTool.save_resp_img(
69
72
  self,
70
73
  path,
71
- need_convert=suffix_not_equal(img_url[:img_url.find("?")], path),
74
+ need_convert=suffix_not_equal(img_url, path),
72
75
  )
73
76
  else:
74
77
  # 解密图片并保存文件
@@ -414,7 +414,7 @@ class JmModuleConfig:
414
414
  'cache': None, # see CacheRegistry
415
415
  'domain': [],
416
416
  'postman': {
417
- 'type': 'cffi',
417
+ 'type': 'curl_cffi',
418
418
  'meta_data': {
419
419
  'impersonate': 'chrome110',
420
420
  'headers': None,
@@ -1,6 +1,31 @@
1
1
  from .jm_option import *
2
2
 
3
3
 
4
+ def catch_exception(func):
5
+ from functools import wraps
6
+
7
+ @wraps(func)
8
+ def wrapper(self, *args, **kwargs):
9
+ self: JmDownloader
10
+ try:
11
+ return func(self, *args, **kwargs)
12
+ except Exception as e:
13
+ detail: JmBaseEntity = args[0]
14
+ if detail.is_image():
15
+ detail: JmImageDetail
16
+ jm_log('image.failed', f'图片下载失败: [{detail.download_url}], 异常: [{e}]')
17
+ self.download_failed_image.append((detail, e))
18
+
19
+ elif detail.is_photo():
20
+ detail: JmPhotoDetail
21
+ jm_log('photo.failed', f'章节下载失败: [{detail.id}], 异常: [{e}]')
22
+ self.download_failed_photo.append((detail, e))
23
+
24
+ raise e
25
+
26
+ return wrapper
27
+
28
+
4
29
  # noinspection PyMethodMayBeStatic
5
30
  class DownloadCallback:
6
31
 
@@ -50,48 +75,50 @@ class JmDownloader(DownloadCallback):
50
75
 
51
76
  def __init__(self, option: JmOption) -> None:
52
77
  self.option = option
78
+ self.client = option.build_jm_client()
53
79
  # 下载成功的记录dict
54
80
  self.download_success_dict: Dict[JmAlbumDetail, Dict[JmPhotoDetail, List[Tuple[str, JmImageDetail]]]] = {}
55
81
  # 下载失败的记录list
56
- self.download_failed_list: List[Tuple[JmImageDetail, BaseException]] = []
82
+ self.download_failed_image: List[Tuple[JmImageDetail, BaseException]] = []
83
+ self.download_failed_photo: List[Tuple[JmPhotoDetail, BaseException]] = []
57
84
 
58
85
  def download_album(self, album_id):
59
- client = self.client_for_album(album_id)
60
- album = client.get_album_detail(album_id)
61
- self.download_by_album_detail(album, client)
86
+ album = self.client.get_album_detail(album_id)
87
+ self.download_by_album_detail(album)
62
88
  return album
63
89
 
64
- def download_by_album_detail(self, album: JmAlbumDetail, client: JmcomicClient):
90
+ def download_by_album_detail(self, album: JmAlbumDetail):
65
91
  self.before_album(album)
66
92
  if album.skip:
67
93
  return
68
- self.execute_by_condition(
94
+ self.execute_on_condition(
69
95
  iter_objs=album,
70
- apply=lambda photo: self.download_by_photo_detail(photo, client),
96
+ apply=self.download_by_photo_detail,
71
97
  count_batch=self.option.decide_photo_batch_count(album)
72
98
  )
73
99
  self.after_album(album)
74
100
 
75
101
  def download_photo(self, photo_id):
76
- client = self.client_for_photo(photo_id)
77
- photo = client.get_photo_detail(photo_id)
78
- self.download_by_photo_detail(photo, client)
102
+ photo = self.client.get_photo_detail(photo_id)
103
+ self.download_by_photo_detail(photo)
79
104
  return photo
80
105
 
81
- def download_by_photo_detail(self, photo: JmPhotoDetail, client: JmcomicClient):
82
- client.check_photo(photo)
106
+ @catch_exception
107
+ def download_by_photo_detail(self, photo: JmPhotoDetail):
108
+ self.client.check_photo(photo)
83
109
 
84
110
  self.before_photo(photo)
85
111
  if photo.skip:
86
112
  return
87
- self.execute_by_condition(
113
+ self.execute_on_condition(
88
114
  iter_objs=photo,
89
- apply=lambda image: self.download_by_image_detail(image, client),
115
+ apply=self.download_by_image_detail,
90
116
  count_batch=self.option.decide_image_batch_count(photo)
91
117
  )
92
118
  self.after_photo(photo)
93
119
 
94
- def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
120
+ @catch_exception
121
+ def download_by_image_detail(self, image: JmImageDetail):
95
122
  img_save_path = self.option.decide_image_filepath(image)
96
123
 
97
124
  image.save_path = img_save_path
@@ -110,22 +137,15 @@ class JmDownloader(DownloadCallback):
110
137
  if use_cache is True and image.exists:
111
138
  return
112
139
 
113
- try:
114
- client.download_by_image_detail(
115
- image,
116
- img_save_path,
117
- decode_image=decode_image,
118
- )
119
- except BaseException as e:
120
- jm_log('image.failed', f'图片下载失败: [{image.download_url}], 异常: {e}')
121
- # 保存失败记录
122
- self.download_failed_list.append((image, e))
123
- raise
140
+ self.client.download_by_image_detail(
141
+ image,
142
+ img_save_path,
143
+ decode_image=decode_image,
144
+ )
124
145
 
125
146
  self.after_image(image, img_save_path)
126
147
 
127
- # noinspection PyMethodMayBeStatic
128
- def execute_by_condition(self,
148
+ def execute_on_condition(self,
129
149
  iter_objs: DetailEntity,
130
150
  apply: Callable,
131
151
  count_batch: int,
@@ -166,20 +186,6 @@ class JmDownloader(DownloadCallback):
166
186
  """
167
187
  return detail
168
188
 
169
- # noinspection PyUnusedLocal
170
- def client_for_album(self, jm_album_id) -> JmcomicClient:
171
- """
172
- 默认情况下,所有的JmDownloader共用一个JmcomicClient
173
- """
174
- return self.option.build_jm_client()
175
-
176
- # noinspection PyUnusedLocal
177
- def client_for_photo(self, jm_photo_id) -> JmcomicClient:
178
- """
179
- 默认情况下,所有的JmDownloader共用一个JmcomicClient
180
- """
181
- return self.option.build_jm_client()
182
-
183
189
  @property
184
190
  def all_success(self) -> bool:
185
191
  """
@@ -189,7 +195,7 @@ class JmDownloader(DownloadCallback):
189
195
 
190
196
  注意!如果使用了filter机制,例如通过filter只下载3张图片,那么all_success也会为False
191
197
  """
192
- if len(self.download_failed_list) != 0:
198
+ if self.has_download_failures:
193
199
  return False
194
200
 
195
201
  for album, photo_dict in self.download_success_dict.items():
@@ -202,6 +208,10 @@ class JmDownloader(DownloadCallback):
202
208
 
203
209
  return True
204
210
 
211
+ @property
212
+ def has_download_failures(self):
213
+ return len(self.download_failed_image) != 0 or len(self.download_failed_photo) != 0
214
+
205
215
  # 下面是回调方法
206
216
 
207
217
  def before_album(self, album: JmAlbumDetail):
@@ -259,6 +269,23 @@ class JmDownloader(DownloadCallback):
259
269
  downloader=self,
260
270
  )
261
271
 
272
+ def raise_if_has_exception(self):
273
+ if not self.has_download_failures:
274
+ return
275
+ msg_ls = ['部分下载失败', '', '']
276
+
277
+ if len(self.download_failed_photo) != 0:
278
+ msg_ls[1] = f'共{len(self.download_failed_photo)}个章节下载失败: {self.download_failed_photo}'
279
+
280
+ if len(self.download_failed_image) != 0:
281
+ msg_ls[2] = f'共{len(self.download_failed_image)}个图片下载失败: {self.download_failed_image}'
282
+
283
+ ExceptionTool.raises(
284
+ '\n'.join(msg_ls),
285
+ {'downloader': self},
286
+ PartialDownloadFailedException,
287
+ )
288
+
262
289
  # 下面是对with语法的支持
263
290
 
264
291
  def __enter__(self):
@@ -283,7 +310,7 @@ class DoNotDownloadImage(JmDownloader):
283
310
  不会下载任何图片的Downloader,用作测试
284
311
  """
285
312
 
286
- def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
313
+ def download_by_image_detail(self, image: JmImageDetail):
287
314
  # ensure make dir
288
315
  self.option.decide_image_filepath(image)
289
316
 
@@ -297,12 +324,13 @@ class JustDownloadSpecificCountImage(JmDownloader):
297
324
  count_lock = Lock()
298
325
  count = 0
299
326
 
300
- def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
327
+ @catch_exception
328
+ def download_by_image_detail(self, image: JmImageDetail):
301
329
  # ensure make dir
302
330
  self.option.decide_image_filepath(image)
303
331
 
304
332
  if self.try_countdown():
305
- return super().download_by_image_detail(image, client)
333
+ return super().download_by_image_detail(image)
306
334
 
307
335
  def try_countdown(self):
308
336
  if self.count < 0:
@@ -125,10 +125,9 @@ class DetailEntity(JmBaseEntity, IndexedEntity):
125
125
  return f'[{self.id}] {self.oname}'
126
126
 
127
127
  def __str__(self):
128
- return f'{self.__class__.__name__}' \
129
- '{' \
130
- f'{self.id}: {self.title}' \
131
- '}'
128
+ return f'''{self.__class__.__name__}({self.__alias__()}-{self.id}: "{self.title}")'''
129
+
130
+ __repr__ = __str__
132
131
 
133
132
  @classmethod
134
133
  def __alias__(cls):
@@ -165,6 +164,32 @@ class DetailEntity(JmBaseEntity, IndexedEntity):
165
164
 
166
165
  return getattr(detail, ref)
167
166
 
167
+ def get_properties_dict(self):
168
+ import inspect
169
+
170
+ prefix = self.__class__.__name__[2]
171
+ result = {}
172
+
173
+ # field
174
+ for k, v in self.__dict__.items():
175
+ result[prefix + k] = v
176
+
177
+ # property
178
+ for cls in inspect.getmro(type(self)):
179
+ for name, attr in cls.__dict__.items():
180
+ k = prefix + name
181
+ if k not in result and isinstance(attr, property):
182
+ v = attr.__get__(self, cls)
183
+ result[k] = v
184
+
185
+ # advice
186
+ advice_dict = JmModuleConfig.AFIELD_ADVICE if self.is_album() else JmModuleConfig.PFIELD_ADVICE
187
+ for name, func in advice_dict.items():
188
+ k = prefix + name
189
+ result[k] = func(self)
190
+
191
+ return result
192
+
168
193
 
169
194
  class JmImageDetail(JmBaseEntity, Downloadable):
170
195
 
@@ -258,6 +283,11 @@ class JmImageDetail(JmBaseEntity, Downloadable):
258
283
  def is_image(cls):
259
284
  return True
260
285
 
286
+ def __str__(self):
287
+ return f'''{self.__class__.__name__}(image-[{self.download_url}])'''
288
+
289
+ __repr__ = __str__
290
+
261
291
 
262
292
  class JmPhotoDetail(DetailEntity, Downloadable):
263
293
 
@@ -15,6 +15,7 @@ class JmcomicException(Exception):
15
15
  def __str__(self):
16
16
  return self.msg
17
17
 
18
+
18
19
  class ResponseUnexpectedException(JmcomicException):
19
20
  description = '响应不符合预期异常'
20
21
 
@@ -44,7 +45,6 @@ class RegularNotMatchException(JmcomicException):
44
45
 
45
46
  class JsonResolveFailException(ResponseUnexpectedException):
46
47
  description = 'Json解析异常'
47
- pass
48
48
 
49
49
 
50
50
  class MissingAlbumPhotoException(ResponseUnexpectedException):
@@ -57,9 +57,15 @@ class MissingAlbumPhotoException(ResponseUnexpectedException):
57
57
 
58
58
  class RequestRetryAllFailException(JmcomicException):
59
59
  description = '请求重试全部失败异常'
60
- pass
61
60
 
62
61
 
62
+ class PartialDownloadFailedException(JmcomicException):
63
+ description = '部分章节或图片下载失败异常'
64
+
65
+ @property
66
+ def downloader(self):
67
+ return self.from_context(ExceptionTool.CONTEXT_KEY_DOWNLOADER)
68
+
63
69
  class ExceptionTool:
64
70
  """
65
71
  抛异常的工具
@@ -71,6 +77,7 @@ class ExceptionTool:
71
77
  CONTEXT_KEY_HTML = 'html'
72
78
  CONTEXT_KEY_RE_PATTERN = 'pattern'
73
79
  CONTEXT_KEY_MISSING_JM_ID = 'missing_jm_id'
80
+ CONTEXT_KEY_DOWNLOADER = 'downloader'
74
81
 
75
82
  @classmethod
76
83
  def raises(cls,
@@ -71,7 +71,7 @@ class DirRule:
71
71
  ]
72
72
 
73
73
  Detail = Union[JmAlbumDetail, JmPhotoDetail, None]
74
- RuleFunc = Callable[[Detail], str]
74
+ RuleFunc = Callable
75
75
  RuleSolver = Tuple[str, RuleFunc, str]
76
76
  RuleSolverList = List[RuleSolver]
77
77
 
@@ -154,6 +154,12 @@ class DirRule:
154
154
 
155
155
  @classmethod
156
156
  def get_rule_solver(cls, rule: str) -> Optional[RuleSolver]:
157
+ if '{' in rule:
158
+ def format_path(album, photo):
159
+ return fix_windir_name(rule.format(**album.get_properties_dict(), **photo.get_properties_dict())).strip()
160
+
161
+ return 'F', format_path, rule
162
+
157
163
  # 检查dsl
158
164
  if not rule.startswith(('A', 'P')):
159
165
  return None
@@ -176,15 +182,17 @@ class DirRule:
176
182
 
177
183
  def choose_detail(key):
178
184
  if key == 'Bd':
179
- return None
185
+ return None,
180
186
  if key == 'A':
181
- return album
187
+ return album,
182
188
  if key == 'P':
183
- return photo
189
+ return photo,
190
+ if key == 'F':
191
+ return album, photo
184
192
 
185
193
  key, func, _ = rule_solver
186
194
  detail = choose_detail(key)
187
- return func(detail)
195
+ return func(*detail)
188
196
 
189
197
  @classmethod
190
198
  def apply_rule_directly(cls, album, photo, rule: str) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmcomic
3
- Version: 2.5.34
3
+ Version: 2.5.36
4
4
  Summary: Python API For JMComic (禁漫天堂)
5
5
  Home-page: https://github.com/hect0x7/JMComic-Crawler-Python
6
6
  Author: hect0x7
File without changes
File without changes
File without changes
File without changes
File without changes