jmcomic 2.4.5__py3-none-any.whl → 2.4.7__py3-none-any.whl

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.
jmcomic/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
  # 被依赖方 <--- 使用方
3
3
  # config <--- entity <--- toolkit <--- client <--- option <--- downloader
4
4
 
5
- __version__ = '2.4.5'
5
+ __version__ = '2.4.7'
6
6
 
7
7
  from .api import *
8
8
  from .jm_plugin import *
jmcomic/jm_client_impl.py CHANGED
@@ -1,3 +1,5 @@
1
+ from threading import Lock
2
+
1
3
  from .jm_client_interface import *
2
4
 
3
5
 
@@ -25,6 +27,7 @@ class AbstractJmClient(
25
27
  self.retry_times = retry_times
26
28
  self.domain_list = domain_list
27
29
  self.CLIENT_CACHE = None
30
+ self.__username = None # help for favorite_folder method
28
31
  self.enable_cache()
29
32
  self.after_init()
30
33
 
@@ -50,7 +53,7 @@ class AbstractJmClient(
50
53
  resp.require_success()
51
54
  return resp
52
55
 
53
- return self.get(img_url, judge=judge)
56
+ return self.get(img_url, judge=judge, headers=JmModuleConfig.new_html_headers())
54
57
 
55
58
  def request_with_retry(self,
56
59
  request,
@@ -61,7 +64,12 @@ class AbstractJmClient(
61
64
  **kwargs,
62
65
  ):
63
66
  """
64
- 统一请求,支持重试
67
+ 支持重试和切换域名的机制
68
+
69
+ 如果url包含了指定域名,则不会切换域名,例如图片URL。
70
+
71
+ 如果需要拿到域名进行回调处理,可以重写 self.update_request_with_specify_domain 方法,例如更新headers
72
+
65
73
  :param request: 请求方法
66
74
  :param url: 图片url / path (/album/xxx)
67
75
  :param domain_index: 域名下标
@@ -74,10 +82,11 @@ class AbstractJmClient(
74
82
 
75
83
  if url.startswith('/'):
76
84
  # path → url
77
- url = self.of_api_url(
78
- api_path=url,
79
- domain=self.domain_list[domain_index],
80
- )
85
+ domain = self.domain_list[domain_index]
86
+ url = self.of_api_url(url, domain)
87
+
88
+ self.update_request_with_specify_domain(kwargs, domain)
89
+
81
90
  jm_log(self.log_topic(), self.decode(url))
82
91
  else:
83
92
  # 图片url
@@ -96,6 +105,8 @@ class AbstractJmClient(
96
105
  try:
97
106
  resp = request(url, **kwargs)
98
107
  return judge(resp)
108
+ except KeyboardInterrupt as e:
109
+ raise e
99
110
  except Exception as e:
100
111
  if self.retry_times == 0:
101
112
  raise e
@@ -107,6 +118,12 @@ class AbstractJmClient(
107
118
  else:
108
119
  return self.request_with_retry(request, url, domain_index + 1, 0, judge, **kwargs)
109
120
 
121
+ def update_request_with_specify_domain(self, kwargs: dict, domain: str):
122
+ """
123
+ 域名自动切换时,用于更新请求参数的回调
124
+ """
125
+ pass
126
+
110
127
  # noinspection PyMethodMayBeStatic
111
128
  def log_topic(self):
112
129
  return self.client_key
@@ -205,6 +222,34 @@ class JmHtmlClient(AbstractJmClient):
205
222
 
206
223
  func_to_cache = ['search', 'fetch_detail_entity']
207
224
 
225
+ def add_favorite_album(self,
226
+ album_id,
227
+ folder_id='0',
228
+ ):
229
+ data = {
230
+ 'album_id': album_id,
231
+ 'fid': folder_id,
232
+ }
233
+
234
+ resp = self.get_jm_html(
235
+ '/ajax/favorite_album',
236
+ data=data,
237
+ )
238
+
239
+ res = resp.json()
240
+
241
+ if res['status'] != 1:
242
+ msg = parse_unicode_escape_text(res['msg'])
243
+ error_msg = PatternTool.match_or_default(msg, JmcomicText.pattern_ajax_favorite_msg, msg)
244
+ # 此圖片已經在您最喜愛的清單!
245
+
246
+ self.raise_request_error(
247
+ resp,
248
+ error_msg
249
+ )
250
+
251
+ return resp
252
+
208
253
  def get_album_detail(self, album_id) -> JmAlbumDetail:
209
254
  return self.fetch_detail_entity(album_id, 'album')
210
255
 
@@ -301,6 +346,7 @@ class JmHtmlClient(AbstractJmClient):
301
346
  return resp
302
347
 
303
348
  self['cookies'] = new_cookies
349
+ self.__username = username
304
350
 
305
351
  return resp
306
352
 
@@ -311,7 +357,8 @@ class JmHtmlClient(AbstractJmClient):
311
357
  username='',
312
358
  ) -> JmFavoritePage:
313
359
  if username == '':
314
- username = self.get_username_or_raise()
360
+ ExceptionTool.require_true(self.__username is not None, 'favorite_folder方法需要传username参数')
361
+ username = self.__username
315
362
 
316
363
  resp = self.get_jm_html(
317
364
  f'/user/{username}/favorite/albums',
@@ -325,13 +372,12 @@ class JmHtmlClient(AbstractJmClient):
325
372
  return JmPageTool.parse_html_to_favorite_page(resp.text)
326
373
 
327
374
  # noinspection PyTypeChecker
328
- def get_username_or_raise(self) -> str:
329
- cookies = self.get_meta_data('cookies', None)
330
- if not cookies:
331
- ExceptionTool.raises('未登录,无法获取到对应的用户名,需要传username参数')
332
-
375
+ def get_username_from_cookies(self) -> str:
376
+ # cookies = self.get_meta_data('cookies', None)
377
+ # if not cookies:
378
+ # ExceptionTool.raises('未登录,无法获取到对应的用户名,请给favorite方法传入username参数')
333
379
  # 解析cookies,可能需要用到 phpserialize,比较麻烦,暂不实现
334
- ExceptionTool.raises('需要传username参数')
380
+ pass
335
381
 
336
382
  def get_jm_html(self, url, require_200=True, **kwargs):
337
383
  """
@@ -351,6 +397,12 @@ class JmHtmlClient(AbstractJmClient):
351
397
 
352
398
  return resp
353
399
 
400
+ def update_request_with_specify_domain(self, kwargs: dict, domain: Optional[str]):
401
+ latest_headers = kwargs.get('headers', None)
402
+ base_headers = self.get_meta_data('headers', None) or JmModuleConfig.new_html_headers(domain)
403
+ base_headers.update(latest_headers or {})
404
+ kwargs['headers'] = base_headers
405
+
354
406
  @classmethod
355
407
  def raise_request_error(cls, resp, msg: Optional[str] = None):
356
408
  """
@@ -393,10 +445,7 @@ class JmHtmlClient(AbstractJmClient):
393
445
  (f' to ({comment_id})' if comment_id is not None else '')
394
446
  )
395
447
 
396
- resp = self.post('/ajax/album_comment',
397
- headers=self.album_comment_headers,
398
- data=data,
399
- )
448
+ resp = self.post('/ajax/album_comment', data=data)
400
449
 
401
450
  ret = JmAlbumCommentResp(resp)
402
451
  jm_log('album.comment', f'{video_id}: [{comment}] ← ({ret.model().cid})')
@@ -469,26 +518,6 @@ class JmHtmlClient(AbstractJmClient):
469
518
  + (f'URL=[{url}]' if url is not None else '')
470
519
  )
471
520
 
472
- album_comment_headers = {
473
- 'authority': '18comic.vip',
474
- 'accept': 'application/json, text/javascript, */*; q=0.01',
475
- 'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
476
- 'cache-control': 'no-cache',
477
- 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
478
- 'origin': 'https://18comic.vip',
479
- 'pragma': 'no-cache',
480
- 'referer': 'https://18comic.vip/album/248965/',
481
- 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
482
- 'sec-ch-ua-mobile': '?0',
483
- 'sec-ch-ua-platform': '"Windows"',
484
- 'sec-fetch-dest': 'empty',
485
- 'sec-fetch-mode': 'cors',
486
- 'sec-fetch-site': 'same-origin',
487
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
488
- 'Chrome/114.0.0.0 Safari/537.36',
489
- 'x-requested-with': 'XMLHttpRequest',
490
- }
491
-
492
521
 
493
522
  # 基于禁漫移动端(APP)实现的JmClient
494
523
  class JmApiClient(AbstractJmClient):
@@ -581,8 +610,6 @@ class JmApiClient(AbstractJmClient):
581
610
  },
582
611
  )
583
612
 
584
- self.require_resp_success(resp, url)
585
-
586
613
  return JmApiAdaptTool.parse_entity(resp.res_data, clazz)
587
614
 
588
615
  def fetch_scramble_id(self, photo_id):
@@ -600,6 +627,7 @@ class JmApiClient(AbstractJmClient):
600
627
  'express': 'off',
601
628
  'v': time_stamp(),
602
629
  },
630
+ require_success=False,
603
631
  )
604
632
 
605
633
  scramble_id = PatternTool.match_or_default(resp.text,
@@ -712,7 +740,6 @@ class JmApiClient(AbstractJmClient):
712
740
  'password': password,
713
741
  })
714
742
 
715
- resp.require_success()
716
743
  cookies = dict(resp.resp.cookies)
717
744
  cookies.update({'AVS': resp.res_data['s']})
718
745
  self['cookies'] = cookies
@@ -736,7 +763,34 @@ class JmApiClient(AbstractJmClient):
736
763
 
737
764
  return JmPageTool.parse_api_to_favorite_page(resp.model_data)
738
765
 
739
- def req_api(self, url, get=True, **kwargs) -> JmApiResp:
766
+ def add_favorite_album(self,
767
+ album_id,
768
+ folder_id='0',
769
+ ):
770
+ """
771
+ 移动端没有提供folder_id参数
772
+ """
773
+ resp = self.req_api(
774
+ '/favorite',
775
+ data={
776
+ 'aid': album_id,
777
+ },
778
+ )
779
+
780
+ self.require_resp_status_ok(resp)
781
+
782
+ return resp
783
+
784
+ # noinspection PyMethodMayBeStatic
785
+ def require_resp_status_ok(self, resp: JmApiResp):
786
+ """
787
+ 检查返回数据中的status字段是否为ok
788
+ """
789
+ data = resp.model_data
790
+ if data.status == 'ok':
791
+ ExceptionTool.raises_resp(data.msg, resp)
792
+
793
+ def req_api(self, url, get=True, require_success=True, **kwargs) -> JmApiResp:
740
794
  ts = self.decide_headers_and_ts(kwargs, url)
741
795
 
742
796
  if get:
@@ -744,7 +798,15 @@ class JmApiClient(AbstractJmClient):
744
798
  else:
745
799
  resp = self.post(url, **kwargs)
746
800
 
747
- return JmApiResp(resp, ts)
801
+ resp = JmApiResp(resp, ts)
802
+
803
+ if require_success:
804
+ self.require_resp_success(resp, url)
805
+
806
+ return resp
807
+
808
+ def update_request_with_specify_domain(self, kwargs: dict, domain: str):
809
+ pass
748
810
 
749
811
  # noinspection PyMethodMayBeStatic
750
812
  def decide_headers_and_ts(self, kwargs, url):
@@ -791,7 +853,6 @@ class JmApiClient(AbstractJmClient):
791
853
  if JmModuleConfig.flag_api_client_require_cookies:
792
854
  self.ensure_have_cookies()
793
855
 
794
- from threading import Lock
795
856
  client_init_cookies_lock = Lock()
796
857
 
797
858
  def ensure_have_cookies(self):
@@ -826,9 +887,6 @@ class FutureClientProxy(JmcomicClient):
826
887
  ```
827
888
  """
828
889
  client_key = 'cl_proxy_future'
829
- proxy_methods = ['album_comment', 'enable_cache', 'get_domain_list',
830
- 'get_html_domain', 'get_html_domain_all', 'get_jm_image',
831
- 'set_cache_dict', 'get_cache_dict', 'set_domain_list', ]
832
890
 
833
891
  class FutureWrapper:
834
892
  def __init__(self, future, after_done_callback):
@@ -855,8 +913,7 @@ class FutureClientProxy(JmcomicClient):
855
913
  executors=None,
856
914
  ):
857
915
  self.client = client
858
- for method in self.proxy_methods:
859
- setattr(self, method, getattr(client, method))
916
+ self.route_notimpl_method_to_internal_client(client)
860
917
 
861
918
  if executors is None:
862
919
  from concurrent.futures import ThreadPoolExecutor
@@ -867,6 +924,25 @@ class FutureClientProxy(JmcomicClient):
867
924
  from threading import Lock
868
925
  self.lock = Lock()
869
926
 
927
+ def route_notimpl_method_to_internal_client(self, client):
928
+
929
+ impl_methods = str_to_set('''
930
+ get_album_detail
931
+ get_photo_detail
932
+ search
933
+ ''')
934
+
935
+ # 获取对象的所有属性和方法的名称列表
936
+ attributes_and_methods = dir(client)
937
+ # 遍历属性和方法列表,并访问每个方法
938
+ for method in attributes_and_methods:
939
+ # 判断是否为方法(可调用对象)
940
+ if (not method.startswith('_')
941
+ and callable(getattr(client, method))
942
+ and method not in impl_methods
943
+ ):
944
+ setattr(self, method, getattr(client, method))
945
+
870
946
  def get_album_detail(self, album_id) -> JmAlbumDetail:
871
947
  album_id = JmcomicText.parse_to_jm_id(album_id)
872
948
  cache_key = f'album_{album_id}'
@@ -229,6 +229,15 @@ class JmUserClient:
229
229
  """
230
230
  raise NotImplementedError
231
231
 
232
+ def add_favorite_album(self,
233
+ album_id,
234
+ folder_id='0',
235
+ ):
236
+ """
237
+ 把漫画加入收藏夹
238
+ """
239
+ raise NotImplementedError
240
+
232
241
 
233
242
  class JmImageClient:
234
243
 
jmcomic/jm_config.py CHANGED
@@ -151,6 +151,12 @@ class JmModuleConfig:
151
151
  # log时解码url
152
152
  flag_decode_url_when_logging = True
153
153
 
154
+ # 关联dir_rule的自定义字段与对应的处理函数
155
+ # 例如:
156
+ # Amyname -> JmModuleConfig.AFIELD_ADVICE['myname'] = lambda album: "自定义名称"
157
+ AFIELD_ADVICE = dict()
158
+ PFIELD_ADVICE = dict()
159
+
154
160
  @classmethod
155
161
  def downloader_class(cls):
156
162
  if cls.CLASS_DOWNLOADER is not None:
@@ -254,6 +260,7 @@ class JmModuleConfig:
254
260
  headers = JmMagicConstants.HTML_HEADERS_TEMPLATE.copy()
255
261
  headers.update({
256
262
  'authority': domain,
263
+ 'origin': f'https://{domain}',
257
264
  'referer': f'https://{domain}',
258
265
  })
259
266
  return headers
@@ -290,9 +297,12 @@ class JmModuleConfig:
290
297
  return Postmans.new_postman(**kwargs)
291
298
 
292
299
  # option 相关的默认配置
300
+ # 一般情况下,建议使用option配置文件来定制配置
301
+ # 而如果只想修改几个简单常用的配置,也可以下方的DEFAULT_XXX属性
293
302
  JM_OPTION_VER = '2.1'
294
- DEFAULT_CLIENT_IMPL = 'html'
295
- DEFAULT_PROXIES = ProxyBuilder.system_proxy() # use system proxy by default
303
+ DEFAULT_CLIENT_IMPL = 'html' # 默认Client实现类型为网页端
304
+ DEFAULT_CLIENT_CACHE = True # 默认开启Client缓存,缓存级别是level_option,详见CacheRegistry
305
+ DEFAULT_PROXIES = ProxyBuilder.system_proxy() # 默认使用系统代理
296
306
 
297
307
  default_option_dict: dict = {
298
308
  'log': None,
@@ -349,7 +359,7 @@ class JmModuleConfig:
349
359
  # client cache
350
360
  client = option_dict['client']
351
361
  if client['cache'] is None:
352
- client['cache'] = True
362
+ client['cache'] = cls.DEFAULT_CLIENT_CACHE
353
363
 
354
364
  # client impl
355
365
  if client['impl'] is None:
jmcomic/jm_downloader.py CHANGED
@@ -115,7 +115,7 @@ class JmDownloader(DownloadCallback):
115
115
  """
116
116
  调度本子/章节的下载
117
117
  """
118
- iter_objs = self.filter_iter_objs(iter_objs)
118
+ iter_objs = self.do_filter(iter_objs)
119
119
  count_real = len(iter_objs)
120
120
 
121
121
  if count_real == 0:
@@ -136,14 +136,14 @@ class JmDownloader(DownloadCallback):
136
136
  )
137
137
 
138
138
  # noinspection PyMethodMayBeStatic
139
- def filter_iter_objs(self, detail: DetailEntity):
139
+ def do_filter(self, detail: DetailEntity):
140
140
  """
141
141
  该方法可用于过滤本子/章节,默认不会做过滤。
142
142
  例如:
143
143
  只想下载 本子的最新一章,返回 [album[-1]]
144
144
  只想下载 章节的前10张图片,返回 [photo[:10]]
145
145
 
146
- :param detail: 可能是本子或者章节,需要自行使用 isinstance / is_xxx 判断
146
+ :param detail: 可能是本子或者章节,需要自行使用 isinstance / detail.is_xxx 判断
147
147
  :returns: 只想要下载的 本子的章节 或 章节的图片
148
148
  """
149
149
  return detail
@@ -198,3 +198,18 @@ class JmDownloader(DownloadCallback):
198
198
  jm_log('dler.exception',
199
199
  f'{self.__class__.__name__} Exit with exception: {exc_type, exc_val}'
200
200
  )
201
+
202
+
203
+ class DoNotDownloadImage(JmDownloader):
204
+ """
205
+ 本类仅用于测试
206
+
207
+ 用法:
208
+
209
+ JmModuleConfig.CLASS_DOWNLOADER = DoNotDownloadImage
210
+ """
211
+
212
+ def download_by_image_detail(self, image: JmImageDetail, client: JmcomicClient):
213
+ # ensure make dir
214
+ self.option.decide_image_filepath(image)
215
+ pass
jmcomic/jm_entity.py CHANGED
@@ -61,6 +61,58 @@ class DetailEntity(JmBaseEntity, IndexedEntity):
61
61
  def title(self) -> str:
62
62
  return getattr(self, 'name')
63
63
 
64
+ @property
65
+ def author(self):
66
+ raise NotImplementedError
67
+
68
+ @property
69
+ def oname(self) -> str:
70
+ """
71
+ oname = original name
72
+
73
+ 示例:
74
+
75
+ title:"喂我吃吧 老師! [欶瀾漢化組] [BLVEFO9] たべさせて、せんせい! (ブルーアーカイブ) [中國翻譯] [無修正]"
76
+
77
+ oname:"喂我吃吧 老師!"
78
+
79
+ :return: 返回本子的原始名称
80
+ """
81
+ from .jm_toolkit import JmcomicText
82
+ oname = JmcomicText.parse_orig_album_name(self.title)
83
+ if oname is not None:
84
+ return oname
85
+
86
+ jm_log('entity', f'无法提取出原album名字: {self.title}')
87
+ return self.title
88
+
89
+ @property
90
+ def authoroname(self):
91
+ """
92
+ authoroname = author + oname
93
+
94
+ 比较好识别的一种本子名称方式
95
+
96
+ 具体格式: f'【author】{oname}'
97
+
98
+ 示例:
99
+
100
+ 原本子名:喂我吃吧 老師! [欶瀾漢化組] [BLVEFO9] たべさせて、せんせい! (ブルーアーカイブ) [中國翻譯] [無修正]
101
+
102
+ authoroname:【BLVEFO9】喂我吃吧 老師!
103
+
104
+ :return: 返回作者名+作品原名,格式为: '【author】{oname}'
105
+ """
106
+ return f'【{self.author}】{self.oname}'
107
+
108
+ @property
109
+ def idoname(self):
110
+ """
111
+ 类似 authoroname
112
+ :return: '[id] {oname}'
113
+ """
114
+ return f'[{self.id}] {self.oname}'
115
+
64
116
  def __str__(self):
65
117
  return f'{self.__class__.__name__}({self.id}-{self.title})'
66
118
 
@@ -71,19 +123,33 @@ class DetailEntity(JmBaseEntity, IndexedEntity):
71
123
  cls_name = cls.__name__
72
124
  return cls_name[cls_name.index("m") + 1: cls_name.rfind("Detail")].lower()
73
125
 
74
- def get_dirname(self, ref: str) -> str:
126
+ @classmethod
127
+ def get_dirname(cls, detail: 'DetailEntity', ref: str) -> str:
75
128
  """
76
129
  该方法被 DirDule 调用,用于生成特定层次的文件夹
130
+
77
131
  通常调用方式如下:
78
- Atitle -> ref = 'title' -> album.get_dirname(ref)
79
- 该方法需要返回 ref 对应的文件夹名,默认实现直接返回 getattr(self, ref)
132
+ Atitle -> ref = 'title' -> DetailEntity.get_dirname(album, 'title')
133
+ 该方法需要返回 ref 对应的文件夹名,默认实现直接返回 getattr(detail, 'title')
80
134
 
81
135
  用户可重写此方法,来实现自定义文件夹名
82
136
 
137
+ v2.4.5: 此方法支持优先从 JmModuleConfig.XFIELD_ADVICE 中获取自定义函数并调用返回结果
138
+
139
+ :param detail: 本子/章节 实例
83
140
  :param ref: 字段名
84
141
  :returns: 文件夹名
85
142
  """
86
- return getattr(self, ref)
143
+
144
+ advice_func = (JmModuleConfig.AFIELD_ADVICE
145
+ if isinstance(detail, JmAlbumDetail)
146
+ else JmModuleConfig.PFIELD_ADVICE
147
+ ).get(ref, None)
148
+
149
+ if advice_func is not None:
150
+ return advice_func(detail)
151
+
152
+ return getattr(detail, ref)
87
153
 
88
154
 
89
155
  class JmImageDetail(JmBaseEntity):
@@ -109,8 +175,8 @@ class JmImageDetail(JmBaseEntity):
109
175
  self.img_file_suffix: str = img_file_suffix
110
176
 
111
177
  self.from_photo: Optional[JmPhotoDetail] = from_photo
112
- self.query_params: StrNone = query_params
113
- self.index = index
178
+ self.query_params: Optional[str] = query_params
179
+ self.index = index # 从1开始
114
180
 
115
181
  # temp fields, in order to simplify passing parameter
116
182
  self.save_path: str = ''
@@ -171,7 +237,7 @@ class JmImageDetail(JmBaseEntity):
171
237
  """
172
238
  this tag is used to print pretty info when logging
173
239
  """
174
- return f'{self.aid}/{self.img_file_name}{self.img_file_suffix} [{self.index + 1}/{len(self.from_photo)}]'
240
+ return f'{self.aid}/{self.img_file_name}{self.img_file_suffix} [{self.index}/{len(self.from_photo)}]'
175
241
 
176
242
  @classmethod
177
243
  def is_image(cls):
@@ -200,7 +266,7 @@ class JmPhotoDetail(DetailEntity):
200
266
  self._tags: str = tags
201
267
  self._series_id: int = int(series_id)
202
268
 
203
- self._author: StrNone = author
269
+ self._author: Optional[str] = author
204
270
  self.from_album: Optional[JmAlbumDetail] = from_album
205
271
  self.index = self.album_index
206
272
 
@@ -212,7 +278,7 @@ class JmPhotoDetail(DetailEntity):
212
278
  # page_arr存放了该photo的所有图片文件名 img_name
213
279
  self.page_arr: List[str] = page_arr
214
280
  # 图片的cdn域名
215
- self.data_original_domain: StrNone = data_original_domain
281
+ self.data_original_domain: Optional[str] = data_original_domain
216
282
  # 第一张图的URL
217
283
  self.data_original_0 = data_original_0
218
284
 
@@ -289,7 +355,7 @@ class JmPhotoDetail(DetailEntity):
289
355
  data_original,
290
356
  from_photo=self,
291
357
  query_params=self.data_original_query_params,
292
- index=index,
358
+ index=index + 1,
293
359
  )
294
360
 
295
361
  def get_img_data_original(self, img_name: str) -> str:
@@ -306,7 +372,7 @@ class JmPhotoDetail(DetailEntity):
306
372
  return f'{JmModuleConfig.PROT}{domain}/media/photos/{self.photo_id}/{img_name}'
307
373
 
308
374
  # noinspection PyMethodMayBeStatic
309
- def get_data_original_query_params(self, data_original_0: StrNone) -> str:
375
+ def get_data_original_query_params(self, data_original_0: Optional[str]) -> str:
310
376
  if data_original_0 is None:
311
377
  return f'v={time_stamp()}'
312
378
 
@@ -468,12 +534,18 @@ class JmPageContent(JmBaseEntity, IndexedEntity):
468
534
 
469
535
  @property
470
536
  def page_count(self) -> int:
537
+ """
538
+ 页数
539
+ """
471
540
  page_size = self.page_size
472
541
  import math
473
542
  return math.ceil(int(self.total) / page_size)
474
543
 
475
544
  @property
476
545
  def page_size(self) -> int:
546
+ """
547
+ 页大小
548
+ """
477
549
  raise NotImplementedError
478
550
 
479
551
  def iter_id(self) -> Generator[str, None, None]:
jmcomic/jm_option.py CHANGED
@@ -132,7 +132,7 @@ class DirRule:
132
132
 
133
133
  # Axxx or Pyyy
134
134
  key = 1 if rule[0] == 'A' else 2
135
- solve_func = lambda detail, ref=rule[1:]: fix_windir_name(str(detail.get_dirname(ref)))
135
+ solve_func = lambda detail, ref=rule[1:]: fix_windir_name(str(DetailEntity.get_dirname(detail, ref)))
136
136
 
137
137
  # 保存缓存
138
138
  rule_solver = (key, solve_func, rule)
@@ -276,22 +276,10 @@ class JmOption:
276
276
  return JmModuleConfig.option_default_dict()
277
277
 
278
278
  @classmethod
279
- def default(cls, proxies=None, domain=None) -> 'JmOption':
279
+ def default(cls) -> 'JmOption':
280
280
  """
281
281
  使用默认的 JmOption
282
- proxies, domain 为常用配置项,为了方便起见直接支持参数配置。
283
- 其他配置项建议还是使用配置文件
284
- :param proxies: clash; 127.0.0.1:7890; v2ray
285
- :param domain: 18comic.vip; ["18comic.vip"]
286
282
  """
287
- if proxies is not None or domain is not None:
288
- return cls.construct({
289
- 'client': {
290
- 'domain': [domain] if isinstance(domain, str) else domain,
291
- 'postman': {'meta_data': {'proxies': ProxyBuilder.build_by_str(proxies)}},
292
- },
293
- })
294
-
295
283
  return cls.construct({})
296
284
 
297
285
  @classmethod
@@ -372,7 +360,7 @@ class JmOption:
372
360
  """
373
361
  return self.new_jm_client(**kwargs)
374
362
 
375
- def new_jm_client(self, domain=None, impl=None, cache=None, **kwargs) -> JmcomicClient:
363
+ def new_jm_client(self, domain_list=None, impl=None, cache=None, **kwargs) -> JmcomicClient:
376
364
  """
377
365
  创建新的Client(客户端),不同Client之间的元数据不共享
378
366
  """
@@ -380,10 +368,15 @@ class JmOption:
380
368
 
381
369
  # 所有需要用到的 self.client 配置项如下
382
370
  postman_conf: dict = deepcopy(self.client.postman.src_dict) # postman dsl 配置
371
+
383
372
  meta_data: dict = postman_conf['meta_data'] # 元数据
373
+
384
374
  retry_times: int = self.client.retry_times # 重试次数
375
+
385
376
  cache: str = cache if cache is not None else self.client.cache # 启用缓存
377
+
386
378
  impl: str = impl or self.client.impl # client_key
379
+
387
380
  if isinstance(impl, type):
388
381
  # eg: impl = JmHtmlClient
389
382
  # noinspection PyUnresolvedReferences
@@ -392,28 +385,30 @@ class JmOption:
392
385
  # start construct client
393
386
 
394
387
  # domain
395
- def decide_domain():
396
- domain_list: Union[List[str], DictModel, dict] = domain if domain is not None \
397
- else self.client.domain # 域名
388
+ def decide_domain_list():
389
+ nonlocal domain_list
398
390
 
399
- if not isinstance(domain_list, list):
391
+ if domain_list is None:
392
+ domain_list = self.client.domain
393
+
394
+ if not isinstance(domain_list, (list, str)):
395
+ # dict
400
396
  domain_list = domain_list.get(impl, [])
401
397
 
398
+ if isinstance(domain_list, str):
399
+ # multi-lines text
400
+ domain_list = str_to_list(domain_list)
401
+
402
+ # list or str
402
403
  if len(domain_list) == 0:
403
404
  domain_list = self.decide_client_domain(impl)
404
405
 
405
406
  return domain_list
406
407
 
407
- domain: List[str] = decide_domain()
408
-
409
408
  # support kwargs overwrite meta_data
410
409
  if len(kwargs) != 0:
411
410
  meta_data.update(kwargs)
412
411
 
413
- # headers
414
- if meta_data['headers'] is None:
415
- meta_data['headers'] = self.decide_postman_headers(impl, domain[0])
416
-
417
412
  # postman
418
413
  postman = Postmans.create(data=postman_conf)
419
414
 
@@ -424,7 +419,7 @@ class JmOption:
424
419
 
425
420
  client: AbstractJmClient = clazz(
426
421
  postman=postman,
427
- domain_list=domain,
422
+ domain_list=decide_domain_list(),
428
423
  retry_times=retry_times,
429
424
  )
430
425
 
@@ -459,20 +454,6 @@ class JmOption:
459
454
 
460
455
  ExceptionTool.raises(f'没有配置域名,且是无法识别的client类型: {client_key}')
461
456
 
462
- def decide_postman_headers(self, client_key, domain):
463
- is_client_type = lambda ctype: self.client_key_is_given_type(client_key, ctype)
464
-
465
- if is_client_type(JmApiClient):
466
- # 移动端
467
- # 不配置headers,由client每次请求前创建headers
468
- return None
469
-
470
- if is_client_type(JmHtmlClient):
471
- # 网页端
472
- return JmModuleConfig.new_html_headers(domain)
473
-
474
- ExceptionTool.raises(f'没有配置域名,且是无法识别的client类型: {client_key}')
475
-
476
457
  @classmethod
477
458
  def client_key_is_given_type(cls, client_key, ctype: Type[JmcomicClient]):
478
459
  if client_key == ctype.client_key:
jmcomic/jm_plugin.py CHANGED
@@ -208,7 +208,7 @@ class FindUpdatePlugin(JmOptionPlugin):
208
208
  return photo_ls
209
209
 
210
210
  class FindUpdateDownloader(JmDownloader):
211
- def filter_iter_objs(self, detail):
211
+ def do_filter(self, detail):
212
212
  if not detail.is_album():
213
213
  return detail
214
214
 
jmcomic/jm_toolkit.py CHANGED
@@ -55,6 +55,9 @@ class JmcomicText:
55
55
  # 評論(div)
56
56
  pattern_html_album_comment_count = compile(r'<div class="badge"[^>]*?id="total_video_comments">(\d+)</div>'), 0
57
57
 
58
+ # 提取接口返回值信息
59
+ pattern_ajax_favorite_msg = compile(r'</button>(.*?)</div>')
60
+
58
61
  @classmethod
59
62
  def parse_to_jm_domain(cls, text: str):
60
63
  if text.startswith(JmModuleConfig.PROT):
@@ -225,6 +228,73 @@ class JmcomicText:
225
228
  def parse_dsl_text(cls, dsl_text: str) -> str:
226
229
  return cls.dsl_replacer.parse_dsl_text(dsl_text)
227
230
 
231
+ bracket_map = {'(': ')',
232
+ '[': ']',
233
+ '【': '】',
234
+ '(': ')',
235
+ }
236
+
237
+ @classmethod
238
+ def parse_orig_album_name(cls, name: str, default=None):
239
+ word_list = cls.tokenize(name)
240
+
241
+ for word in word_list:
242
+ if word[0] in cls.bracket_map:
243
+ continue
244
+
245
+ return word
246
+
247
+ return default
248
+
249
+ @classmethod
250
+ def tokenize(cls, title: str) -> List[str]:
251
+ """
252
+ 繞道#2 [暴碧漢化組] [えーすけ(123)] よりみち#2 (COMIC 快樂天 2024年1月號) [中國翻譯] [DL版]
253
+ :return: ['繞道#2', '[暴碧漢化組]', '[えーすけ(123)]', 'よりみち#2', '(COMIC 快樂天 2024年1月號)', '[中國翻譯]', '[DL版]']
254
+ """
255
+ title = title.strip()
256
+ ret = []
257
+ bracket_map = cls.bracket_map
258
+
259
+ char_list = []
260
+ i = 0
261
+ length = len(title)
262
+
263
+ def add(w=None):
264
+ if w is None:
265
+ w = ''.join(char_list).strip()
266
+
267
+ if w == '':
268
+ return
269
+
270
+ ret.append(w)
271
+ char_list.clear()
272
+
273
+ while i < length:
274
+ c = title[i]
275
+
276
+ if c in bracket_map:
277
+ # 上一个单词结束
278
+ add()
279
+ # 定位右括号
280
+ j = title.find(bracket_map[c], i)
281
+ ExceptionTool.require_true(j != -1, f'未闭合的 {c}{bracket_map[c]}: {title[i:]}')
282
+ # 整个括号的单词结束
283
+ add(title[i:j + 1])
284
+ # 移动指针
285
+ i = j + 1
286
+ else:
287
+ char_list.append(c)
288
+ i += 1
289
+
290
+ add()
291
+ return ret
292
+
293
+ @classmethod
294
+ def to_zh_cn(cls, s):
295
+ import zhconv
296
+ return zhconv.convert(s, 'zh_cn')
297
+
228
298
 
229
299
  # 支持dsl: #{???} -> os.getenv(???)
230
300
  JmcomicText.dsl_replacer.add_dsl_and_replacer(r'\$\{(.*?)\}', JmcomicText.match_os_env)
@@ -241,7 +311,7 @@ class PatternTool:
241
311
  def require_match(cls, html: str, pattern: Pattern, msg, rindex=1):
242
312
  match = pattern.search(html)
243
313
  if match is not None:
244
- return match[rindex]
314
+ return match[rindex] if rindex is not None else match
245
315
 
246
316
  ExceptionTool.raises_regex(
247
317
  msg,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jmcomic
3
- Version: 2.4.5
3
+ Version: 2.4.7
4
4
  Summary: Python API For JMComic (禁漫天堂)
5
5
  Home-page: https://github.com/hect0x7/JMComic-Crawler-Python
6
6
  Author: hect0x7
@@ -0,0 +1,17 @@
1
+ jmcomic/__init__.py,sha256=BYuf4ruBex9ljlTlGN0xXdUcus_eKTXS7ZLOqkgKJXM,878
2
+ jmcomic/api.py,sha256=yukYd5NCYYxP8K2Gj72iXu7vg9PHgaOvakQzQxEOcO8,2518
3
+ jmcomic/cl.py,sha256=PBSh0JndNFZw3B7WJPj5Y8SeFdKzHE00jIwYo9An-K0,3475
4
+ jmcomic/jm_client_impl.py,sha256=uG4LIZZdz6zAePkgTq80n5pNV-G069cXiSgBRg_dAO8,34106
5
+ jmcomic/jm_client_interface.py,sha256=xaPtHxdzGYmzsnRfq-fkH__hiARNUtxtjtEzD-SwbPg,14120
6
+ jmcomic/jm_config.py,sha256=88YEZyzxBWJcKmbS6sYhfQ4ZoO6kb7ghMYOgUYBfuWg,13110
7
+ jmcomic/jm_downloader.py,sha256=E1M9CS9bYEHWe8SEsdaq_0c3zxQghpkN_4QLZiZ-o1g,7324
8
+ jmcomic/jm_entity.py,sha256=u5aJhIt2Q21re6moXfS0drVBmIJKjNQnRI2lVvbZ-nY,18671
9
+ jmcomic/jm_option.py,sha256=-_Obz2m_roZOAkb1hw62TKDeKIy_iow99z65fXdvX7I,19945
10
+ jmcomic/jm_plugin.py,sha256=ftL0iZbc1GWk75n6ZuP1j-7MaOTqK1mQA04E-rkNEPs,16198
11
+ jmcomic/jm_toolkit.py,sha256=I67hbdHBLVsmi8RFanKy2sOIaMDU5GlOzi7AxfK_8vA,28892
12
+ jmcomic-2.4.7.dist-info/LICENSE,sha256=kz4coTxZxuGxisK3W00tjK57Zh3RcMGq-EnbXrK7-xA,1064
13
+ jmcomic-2.4.7.dist-info/METADATA,sha256=fqfirQgCGD85-12Vhf5hSo71eG3A3DSSSOjzzbi96Nc,5470
14
+ jmcomic-2.4.7.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
15
+ jmcomic-2.4.7.dist-info/entry_points.txt,sha256=tRbQltaGSBjejI0c9jYt-4SXQMd5nSDHcMvHmuTy4ow,44
16
+ jmcomic-2.4.7.dist-info/top_level.txt,sha256=puvVMFYJqIbd6NOTMEvOyugMTT8woBfSQyxEBan3zY4,8
17
+ jmcomic-2.4.7.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- jmcomic/__init__.py,sha256=WjRwSKot4NiVbFVLKejlBlvQ4XkbWqKjIARj_oA7tlc,878
2
- jmcomic/api.py,sha256=yukYd5NCYYxP8K2Gj72iXu7vg9PHgaOvakQzQxEOcO8,2518
3
- jmcomic/cl.py,sha256=PBSh0JndNFZw3B7WJPj5Y8SeFdKzHE00jIwYo9An-K0,3475
4
- jmcomic/jm_client_impl.py,sha256=SoblBkIaXI-TVeQCpNmovgvlN9sknjV0xD_svN5vH8s,32067
5
- jmcomic/jm_client_interface.py,sha256=PaMxxsJvxtUkpq7pRavftMlas3gfC6BQ0ZnFaYiUarU,13886
6
- jmcomic/jm_config.py,sha256=AKYIRA7jVGCppgXebwwVs8Y-Zff2Aw4MT4Gm2QoCW9A,12513
7
- jmcomic/jm_downloader.py,sha256=GjC3JU8-UPvnKd_XlAoW0X-Wt2zGaU5K2bm3PE5R3as,7000
8
- jmcomic/jm_entity.py,sha256=hDiqvuSV-6KApAk6NXQKJHvaVeYGlTrVIFd4uB5Crjs,16621
9
- jmcomic/jm_option.py,sha256=rVrpkQcDZLRnDvHt8xx5YOxzIT14veXVjOsJx99sAnA,21056
10
- jmcomic/jm_plugin.py,sha256=Z-cVc9H3s0Pj91I5ngjx-LdeX2PqYYloOcvogXs4glc,16205
11
- jmcomic/jm_toolkit.py,sha256=t7Pq5apamXory1YAteYxsKOVHFvgbpP6vDaYFdUNYt0,26918
12
- jmcomic-2.4.5.dist-info/LICENSE,sha256=kz4coTxZxuGxisK3W00tjK57Zh3RcMGq-EnbXrK7-xA,1064
13
- jmcomic-2.4.5.dist-info/METADATA,sha256=pkZ_Mg_HLY826TU5TFP_tVJ2S9CpLp3lLuQRvTWMAIc,5470
14
- jmcomic-2.4.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
15
- jmcomic-2.4.5.dist-info/entry_points.txt,sha256=tRbQltaGSBjejI0c9jYt-4SXQMd5nSDHcMvHmuTy4ow,44
16
- jmcomic-2.4.5.dist-info/top_level.txt,sha256=puvVMFYJqIbd6NOTMEvOyugMTT8woBfSQyxEBan3zY4,8
17
- jmcomic-2.4.5.dist-info/RECORD,,