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 +1 -1
- jmcomic/jm_client_impl.py +124 -48
- jmcomic/jm_client_interface.py +9 -0
- jmcomic/jm_config.py +13 -3
- jmcomic/jm_downloader.py +18 -3
- jmcomic/jm_entity.py +83 -11
- jmcomic/jm_option.py +21 -40
- jmcomic/jm_plugin.py +1 -1
- jmcomic/jm_toolkit.py +71 -1
- {jmcomic-2.4.5.dist-info → jmcomic-2.4.7.dist-info}/METADATA +1 -1
- jmcomic-2.4.7.dist-info/RECORD +17 -0
- jmcomic-2.4.5.dist-info/RECORD +0 -17
- {jmcomic-2.4.5.dist-info → jmcomic-2.4.7.dist-info}/LICENSE +0 -0
- {jmcomic-2.4.5.dist-info → jmcomic-2.4.7.dist-info}/WHEEL +0 -0
- {jmcomic-2.4.5.dist-info → jmcomic-2.4.7.dist-info}/entry_points.txt +0 -0
- {jmcomic-2.4.5.dist-info → jmcomic-2.4.7.dist-info}/top_level.txt +0 -0
jmcomic/__init__.py
CHANGED
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
329
|
-
cookies = self.get_meta_data('cookies', None)
|
|
330
|
-
if not cookies:
|
|
331
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}'
|
jmcomic/jm_client_interface.py
CHANGED
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
|
-
|
|
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'] =
|
|
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.
|
|
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
|
|
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
|
-
|
|
126
|
+
@classmethod
|
|
127
|
+
def get_dirname(cls, detail: 'DetailEntity', ref: str) -> str:
|
|
75
128
|
"""
|
|
76
129
|
该方法被 DirDule 调用,用于生成特定层次的文件夹
|
|
130
|
+
|
|
77
131
|
通常调用方式如下:
|
|
78
|
-
Atitle -> ref = 'title' ->
|
|
79
|
-
该方法需要返回 ref 对应的文件夹名,默认实现直接返回 getattr(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
396
|
-
domain_list
|
|
397
|
-
else self.client.domain # 域名
|
|
388
|
+
def decide_domain_list():
|
|
389
|
+
nonlocal domain_list
|
|
398
390
|
|
|
399
|
-
if
|
|
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=
|
|
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
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,
|
|
@@ -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,,
|
jmcomic-2.4.5.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|