jmcomic 2.5.11__py3-none-any.whl → 2.5.14__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 +41 -2
- jmcomic/jm_client_interface.py +29 -6
- jmcomic/jm_config.py +37 -6
- jmcomic/jm_option.py +25 -18
- jmcomic/jm_plugin.py +82 -50
- jmcomic/jm_toolkit.py +3 -3
- {jmcomic-2.5.11.dist-info → jmcomic-2.5.14.dist-info}/METADATA +32 -9
- jmcomic-2.5.14.dist-info/RECORD +18 -0
- jmcomic-2.5.11.dist-info/RECORD +0 -18
- {jmcomic-2.5.11.dist-info → jmcomic-2.5.14.dist-info}/LICENSE +0 -0
- {jmcomic-2.5.11.dist-info → jmcomic-2.5.14.dist-info}/WHEEL +0 -0
- {jmcomic-2.5.11.dist-info → jmcomic-2.5.14.dist-info}/entry_points.txt +0 -0
- {jmcomic-2.5.11.dist-info → jmcomic-2.5.14.dist-info}/top_level.txt +0 -0
jmcomic/__init__.py
CHANGED
jmcomic/jm_client_impl.py
CHANGED
|
@@ -237,6 +237,9 @@ class JmHtmlClient(AbstractJmClient):
|
|
|
237
237
|
|
|
238
238
|
func_to_cache = ['search', 'fetch_detail_entity']
|
|
239
239
|
|
|
240
|
+
API_SEARCH = '/search/photos'
|
|
241
|
+
API_CATEGORY = '/albums'
|
|
242
|
+
|
|
240
243
|
def add_favorite_album(self,
|
|
241
244
|
album_id,
|
|
242
245
|
folder_id='0',
|
|
@@ -304,7 +307,12 @@ class JmHtmlClient(AbstractJmClient):
|
|
|
304
307
|
main_tag: int,
|
|
305
308
|
order_by: str,
|
|
306
309
|
time: str,
|
|
310
|
+
category: str,
|
|
311
|
+
sub_category: Optional[str],
|
|
307
312
|
) -> JmSearchPage:
|
|
313
|
+
"""
|
|
314
|
+
网页搜索API
|
|
315
|
+
"""
|
|
308
316
|
params = {
|
|
309
317
|
'main_tag': main_tag,
|
|
310
318
|
'search_query': search_query,
|
|
@@ -313,8 +321,10 @@ class JmHtmlClient(AbstractJmClient):
|
|
|
313
321
|
't': time,
|
|
314
322
|
}
|
|
315
323
|
|
|
324
|
+
url = self.build_search_url(self.API_SEARCH, category, sub_category)
|
|
325
|
+
|
|
316
326
|
resp = self.get_jm_html(
|
|
317
|
-
self.append_params_to_url(
|
|
327
|
+
self.append_params_to_url(url, params),
|
|
318
328
|
allow_redirects=True,
|
|
319
329
|
)
|
|
320
330
|
|
|
@@ -326,11 +336,31 @@ class JmHtmlClient(AbstractJmClient):
|
|
|
326
336
|
else:
|
|
327
337
|
return JmPageTool.parse_html_to_search_page(resp.text)
|
|
328
338
|
|
|
339
|
+
@classmethod
|
|
340
|
+
def build_search_url(cls, base: str, category: str, sub_category: Optional[str]):
|
|
341
|
+
"""
|
|
342
|
+
构建网页搜索/分类的URL
|
|
343
|
+
|
|
344
|
+
示例:
|
|
345
|
+
:param base: "/search/photos"
|
|
346
|
+
:param category CATEGORY_DOUJIN
|
|
347
|
+
:param sub_category SUB_DOUJIN_CG
|
|
348
|
+
:return "/search/photos/doujin/sub/CG"
|
|
349
|
+
"""
|
|
350
|
+
if category == JmMagicConstants.CATEGORY_ALL:
|
|
351
|
+
return base
|
|
352
|
+
|
|
353
|
+
if sub_category is None:
|
|
354
|
+
return f'{base}/{category}'
|
|
355
|
+
else:
|
|
356
|
+
return f'{base}/{category}/sub/{sub_category}'
|
|
357
|
+
|
|
329
358
|
def categories_filter(self,
|
|
330
359
|
page: int,
|
|
331
360
|
time: str,
|
|
332
361
|
category: str,
|
|
333
362
|
order_by: str,
|
|
363
|
+
sub_category: Optional[str] = None,
|
|
334
364
|
) -> JmCategoryPage:
|
|
335
365
|
params = {
|
|
336
366
|
'page': page,
|
|
@@ -338,7 +368,7 @@ class JmHtmlClient(AbstractJmClient):
|
|
|
338
368
|
't': time,
|
|
339
369
|
}
|
|
340
370
|
|
|
341
|
-
url =
|
|
371
|
+
url = self.build_search_url(self.API_CATEGORY, category, sub_category)
|
|
342
372
|
|
|
343
373
|
resp = self.get_jm_html(
|
|
344
374
|
self.append_params_to_url(url, params),
|
|
@@ -573,7 +603,12 @@ class JmApiClient(AbstractJmClient):
|
|
|
573
603
|
main_tag: int,
|
|
574
604
|
order_by: str,
|
|
575
605
|
time: str,
|
|
606
|
+
category: str,
|
|
607
|
+
sub_category: Optional[str],
|
|
576
608
|
) -> JmSearchPage:
|
|
609
|
+
"""
|
|
610
|
+
移动端暂不支持 category和sub_category
|
|
611
|
+
"""
|
|
577
612
|
params = {
|
|
578
613
|
'main_tag': main_tag,
|
|
579
614
|
'search_query': search_query,
|
|
@@ -603,7 +638,11 @@ class JmApiClient(AbstractJmClient):
|
|
|
603
638
|
time: str,
|
|
604
639
|
category: str,
|
|
605
640
|
order_by: str,
|
|
641
|
+
sub_category: Optional[str] = None,
|
|
606
642
|
):
|
|
643
|
+
"""
|
|
644
|
+
移动端不支持 sub_category
|
|
645
|
+
"""
|
|
607
646
|
# o: mv, mv_m, mv_w, mv_t
|
|
608
647
|
o = f'{order_by}_{time}' if time != JmMagicConstants.TIME_ALL else order_by
|
|
609
648
|
|
jmcomic/jm_client_interface.py
CHANGED
|
@@ -308,9 +308,14 @@ class JmSearchAlbumClient:
|
|
|
308
308
|
main_tag: int,
|
|
309
309
|
order_by: str,
|
|
310
310
|
time: str,
|
|
311
|
+
category: str,
|
|
312
|
+
sub_category: Optional[str],
|
|
311
313
|
) -> JmSearchPage:
|
|
312
314
|
"""
|
|
313
315
|
搜索【成人A漫】
|
|
316
|
+
网页端与移动端的搜索有差别:
|
|
317
|
+
|
|
318
|
+
- 移动端不支持 category, sub_category参数,网页端支持全部参数
|
|
314
319
|
"""
|
|
315
320
|
raise NotImplementedError
|
|
316
321
|
|
|
@@ -319,55 +324,65 @@ class JmSearchAlbumClient:
|
|
|
319
324
|
page: int = 1,
|
|
320
325
|
order_by: str = JmMagicConstants.ORDER_BY_LATEST,
|
|
321
326
|
time: str = JmMagicConstants.TIME_ALL,
|
|
327
|
+
category: str = JmMagicConstants.CATEGORY_ALL,
|
|
328
|
+
sub_category: Optional[str] = None,
|
|
322
329
|
):
|
|
323
330
|
"""
|
|
324
331
|
对应禁漫的站内搜索
|
|
325
332
|
"""
|
|
326
|
-
return self.search(search_query, page, 0, order_by, time)
|
|
333
|
+
return self.search(search_query, page, 0, order_by, time, category, sub_category)
|
|
327
334
|
|
|
328
335
|
def search_work(self,
|
|
329
336
|
search_query: str,
|
|
330
337
|
page: int = 1,
|
|
331
338
|
order_by: str = JmMagicConstants.ORDER_BY_LATEST,
|
|
332
339
|
time: str = JmMagicConstants.TIME_ALL,
|
|
340
|
+
category: str = JmMagicConstants.CATEGORY_ALL,
|
|
341
|
+
sub_category: Optional[str] = None,
|
|
333
342
|
):
|
|
334
343
|
"""
|
|
335
344
|
搜索album的作品 work
|
|
336
345
|
"""
|
|
337
|
-
return self.search(search_query, page, 1, order_by, time)
|
|
346
|
+
return self.search(search_query, page, 1, order_by, time, category, sub_category)
|
|
338
347
|
|
|
339
348
|
def search_author(self,
|
|
340
349
|
search_query: str,
|
|
341
350
|
page: int = 1,
|
|
342
351
|
order_by: str = JmMagicConstants.ORDER_BY_LATEST,
|
|
343
352
|
time: str = JmMagicConstants.TIME_ALL,
|
|
353
|
+
category: str = JmMagicConstants.CATEGORY_ALL,
|
|
354
|
+
sub_category: Optional[str] = None,
|
|
344
355
|
):
|
|
345
356
|
"""
|
|
346
357
|
搜索album的作者 author
|
|
347
358
|
"""
|
|
348
|
-
return self.search(search_query, page, 2, order_by, time)
|
|
359
|
+
return self.search(search_query, page, 2, order_by, time, category, sub_category)
|
|
349
360
|
|
|
350
361
|
def search_tag(self,
|
|
351
362
|
search_query: str,
|
|
352
363
|
page: int = 1,
|
|
353
364
|
order_by: str = JmMagicConstants.ORDER_BY_LATEST,
|
|
354
365
|
time: str = JmMagicConstants.TIME_ALL,
|
|
366
|
+
category: str = JmMagicConstants.CATEGORY_ALL,
|
|
367
|
+
sub_category: Optional[str] = None,
|
|
355
368
|
):
|
|
356
369
|
"""
|
|
357
370
|
搜索album的标签 tag
|
|
358
371
|
"""
|
|
359
|
-
return self.search(search_query, page, 3, order_by, time)
|
|
372
|
+
return self.search(search_query, page, 3, order_by, time, category, sub_category)
|
|
360
373
|
|
|
361
374
|
def search_actor(self,
|
|
362
375
|
search_query: str,
|
|
363
376
|
page: int = 1,
|
|
364
377
|
order_by: str = JmMagicConstants.ORDER_BY_LATEST,
|
|
365
378
|
time: str = JmMagicConstants.TIME_ALL,
|
|
379
|
+
category: str = JmMagicConstants.CATEGORY_ALL,
|
|
380
|
+
sub_category: Optional[str] = None,
|
|
366
381
|
):
|
|
367
382
|
"""
|
|
368
383
|
搜索album的登场角色 actor
|
|
369
384
|
"""
|
|
370
|
-
return self.search(search_query, page, 4, order_by, time)
|
|
385
|
+
return self.search(search_query, page, 4, order_by, time, category, sub_category)
|
|
371
386
|
|
|
372
387
|
|
|
373
388
|
class JmCategoryClient:
|
|
@@ -384,6 +399,7 @@ class JmCategoryClient:
|
|
|
384
399
|
time: str,
|
|
385
400
|
category: str,
|
|
386
401
|
order_by: str,
|
|
402
|
+
sub_category: Optional[str] = None,
|
|
387
403
|
) -> JmCategoryPage:
|
|
388
404
|
"""
|
|
389
405
|
分类
|
|
@@ -391,6 +407,7 @@ class JmCategoryClient:
|
|
|
391
407
|
:param page: 页码
|
|
392
408
|
:param time: 时间范围,默认是全部时间
|
|
393
409
|
:param category: 类别,默认是最新,即显示最新的禁漫本子
|
|
410
|
+
:param sub_category: 副分类,仅网页端有这功能
|
|
394
411
|
:param order_by: 排序方式,默认是观看数
|
|
395
412
|
"""
|
|
396
413
|
raise NotImplementedError
|
|
@@ -522,6 +539,8 @@ class JmcomicClient(
|
|
|
522
539
|
page: int = 1,
|
|
523
540
|
order_by: str = JmMagicConstants.ORDER_BY_LATEST,
|
|
524
541
|
time: str = JmMagicConstants.TIME_ALL,
|
|
542
|
+
category: str = JmMagicConstants.CATEGORY_ALL,
|
|
543
|
+
sub_category: Optional[str] = None,
|
|
525
544
|
) -> Generator[JmSearchPage, Dict, None]:
|
|
526
545
|
"""
|
|
527
546
|
搜索结果的生成器,支持下面这种调用方式:
|
|
@@ -552,6 +571,8 @@ class JmcomicClient(
|
|
|
552
571
|
'main_tag': main_tag,
|
|
553
572
|
'order_by': order_by,
|
|
554
573
|
'time': time,
|
|
574
|
+
'category': category,
|
|
575
|
+
'sub_category': sub_category,
|
|
555
576
|
}
|
|
556
577
|
|
|
557
578
|
yield from self.do_page_iter(params, page, self.search)
|
|
@@ -561,6 +582,7 @@ class JmcomicClient(
|
|
|
561
582
|
time: str = JmMagicConstants.TIME_ALL,
|
|
562
583
|
category: str = JmMagicConstants.CATEGORY_ALL,
|
|
563
584
|
order_by: str = JmMagicConstants.ORDER_BY_LATEST,
|
|
585
|
+
sub_category: Optional[str] = None,
|
|
564
586
|
) -> Generator[JmCategoryPage, Dict, None]:
|
|
565
587
|
"""
|
|
566
588
|
见 search_gen
|
|
@@ -569,6 +591,7 @@ class JmcomicClient(
|
|
|
569
591
|
'time': time,
|
|
570
592
|
'category': category,
|
|
571
593
|
'order_by': order_by,
|
|
594
|
+
'sub_category': sub_category,
|
|
572
595
|
}
|
|
573
596
|
|
|
574
597
|
yield from self.do_page_iter(params, page, self.categories_filter)
|
|
@@ -581,6 +604,6 @@ class JmcomicClient(
|
|
|
581
604
|
"""
|
|
582
605
|
if isinstance(self, ctype):
|
|
583
606
|
return True
|
|
584
|
-
if self.client_key ==
|
|
607
|
+
if self.client_key == ctype.client_key:
|
|
585
608
|
return True
|
|
586
609
|
return False
|
jmcomic/jm_config.py
CHANGED
|
@@ -35,7 +35,29 @@ class JmMagicConstants:
|
|
|
35
35
|
CATEGORY_DOUJIN_COSPLAY = 'doujin_cosplay' # cosplay
|
|
36
36
|
CATEGORY_3D = '3D' # 3D
|
|
37
37
|
CATEGORY_ENGLISH_SITE = 'english_site' # 英文站
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
# 副分类
|
|
40
|
+
SUB_CHINESE = 'chinese' # 汉化,通用副分类
|
|
41
|
+
SUB_JAPANESE = 'japanese' # 日语,通用副分类
|
|
42
|
+
|
|
43
|
+
# 其他类(CATEGORY_ANOTHER)的副分类
|
|
44
|
+
SUB_ANOTHER_OTHER = 'other' # 其他漫画
|
|
45
|
+
SUB_ANOTHER_3D = '3d' # 3D
|
|
46
|
+
SUB_ANOTHER_COSPLAY = 'cosplay' # cosplay
|
|
47
|
+
|
|
48
|
+
# 同人(SUB_CHINESE)的副分类
|
|
49
|
+
SUB_DOUJIN_CG = 'CG' # CG
|
|
50
|
+
SUB_DOUJIN_CHINESE = SUB_CHINESE
|
|
51
|
+
SUB_DOUJIN_JAPANESE = SUB_JAPANESE
|
|
52
|
+
|
|
53
|
+
# 短篇(CATEGORY_SHORT)的副分类
|
|
54
|
+
SUB_SHORT_CHINESE = SUB_CHINESE
|
|
55
|
+
SUB_SHORT_JAPANESE = SUB_JAPANESE
|
|
56
|
+
|
|
57
|
+
# 单本(CATEGORY_SINGLE)的副分类
|
|
58
|
+
SUB_SINGLE_CHINESE = SUB_CHINESE
|
|
59
|
+
SUB_SINGLE_JAPANESE = SUB_JAPANESE
|
|
60
|
+
SUB_SINGLE_YOUTH = 'youth'
|
|
39
61
|
|
|
40
62
|
# 分页大小
|
|
41
63
|
PAGE_SIZE_SEARCH = 80
|
|
@@ -53,7 +75,7 @@ class JmMagicConstants:
|
|
|
53
75
|
APP_TOKEN_SECRET = '18comicAPP'
|
|
54
76
|
APP_TOKEN_SECRET_2 = '18comicAPPContent'
|
|
55
77
|
APP_DATA_SECRET = '185Hcomic3PAPP7R'
|
|
56
|
-
APP_VERSION = '1.
|
|
78
|
+
APP_VERSION = '1.7.0'
|
|
57
79
|
APP_HEADERS_TEMPLATE = {
|
|
58
80
|
'Accept-Encoding': 'gzip',
|
|
59
81
|
'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.11211812; wv) AppleWebKit/537.36 (KHTML, '
|
|
@@ -65,14 +87,20 @@ class JmMagicConstants:
|
|
|
65
87
|
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
|
|
66
88
|
'application/signed-exchange;v=b3;q=0.7',
|
|
67
89
|
'accept-language': 'zh-CN,zh;q=0.9',
|
|
68
|
-
'
|
|
90
|
+
'cache-control': 'no-cache',
|
|
91
|
+
'dnt': '1',
|
|
92
|
+
'pragma': 'no-cache',
|
|
93
|
+
'priority': 'u=0, i',
|
|
94
|
+
'referer': 'https://18comic.vip/',
|
|
95
|
+
'sec-ch-ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
|
|
69
96
|
'sec-ch-ua-mobile': '?0',
|
|
70
97
|
'sec-ch-ua-platform': '"Windows"',
|
|
71
98
|
'sec-fetch-dest': 'document',
|
|
72
99
|
'sec-fetch-mode': 'navigate',
|
|
73
100
|
'sec-fetch-site': 'none',
|
|
74
101
|
'sec-fetch-user': '?1',
|
|
75
|
-
'
|
|
102
|
+
'upgrade-insecure-requests': '1',
|
|
103
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 '
|
|
76
104
|
'Safari/537.36',
|
|
77
105
|
}
|
|
78
106
|
|
|
@@ -109,14 +137,17 @@ class JmModuleConfig:
|
|
|
109
137
|
DOMAIN_IMAGE_LIST = str_to_list('''
|
|
110
138
|
cdn-msp.jmapinodeudzn.net
|
|
111
139
|
cdn-msp2.jmapinodeudzn.net
|
|
140
|
+
cdn-msp2.jmapiproxy3.cc
|
|
141
|
+
cdn-msp3.jmapinodeudzn.net
|
|
112
142
|
|
|
113
143
|
''')
|
|
114
144
|
|
|
115
145
|
# 移动端API域名
|
|
116
146
|
DOMAIN_API_LIST = str_to_list('''
|
|
117
147
|
www.jmapinodeudzn.xyz
|
|
118
|
-
www.
|
|
119
|
-
www.
|
|
148
|
+
www.cdn-eldenringproxy.xyz
|
|
149
|
+
www.cdn-eldenringproxy.me
|
|
150
|
+
www.cdn-eldenringproxy.vip
|
|
120
151
|
www.jmapinode.xyz
|
|
121
152
|
''')
|
|
122
153
|
|
jmcomic/jm_option.py
CHANGED
|
@@ -72,11 +72,9 @@ class DirRule:
|
|
|
72
72
|
|
|
73
73
|
Detail = Union[JmAlbumDetail, JmPhotoDetail, None]
|
|
74
74
|
RuleFunc = Callable[[Detail], str]
|
|
75
|
-
RuleSolver = Tuple[
|
|
75
|
+
RuleSolver = Tuple[str, RuleFunc, str]
|
|
76
76
|
RuleSolverList = List[RuleSolver]
|
|
77
77
|
|
|
78
|
-
rule_solver_cache: Dict[str, RuleSolver] = {}
|
|
79
|
-
|
|
80
78
|
def __init__(self, rule: str, base_dir=None):
|
|
81
79
|
base_dir = JmcomicText.parse_to_abspath(base_dir)
|
|
82
80
|
self.base_dir = base_dir
|
|
@@ -100,6 +98,25 @@ class DirRule:
|
|
|
100
98
|
|
|
101
99
|
return fix_filepath('/'.join(path_ls), is_dir=True)
|
|
102
100
|
|
|
101
|
+
def decide_album_root_dir(self, album: JmAlbumDetail) -> str:
|
|
102
|
+
path_ls = []
|
|
103
|
+
for solver in self.solver_list:
|
|
104
|
+
key, _, rule = solver
|
|
105
|
+
|
|
106
|
+
if key != 'Bd' and key != 'A':
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
ret = self.apply_rule_solver(album, None, solver)
|
|
111
|
+
except BaseException as e:
|
|
112
|
+
# noinspection PyUnboundLocalVariable
|
|
113
|
+
jm_log('dir_rule', f'路径规则"{rule}"的解析出错: {e}, album={album}')
|
|
114
|
+
raise e
|
|
115
|
+
|
|
116
|
+
path_ls.append(str(ret))
|
|
117
|
+
|
|
118
|
+
return fix_filepath('/'.join(path_ls), is_dir=True)
|
|
119
|
+
|
|
103
120
|
def get_role_solver_list(self, rule_dsl: str, base_dir: str) -> RuleSolverList:
|
|
104
121
|
"""
|
|
105
122
|
解析下载路径dsl,得到一个路径规则解析列表
|
|
@@ -111,7 +128,7 @@ class DirRule:
|
|
|
111
128
|
for rule in rule_list:
|
|
112
129
|
rule = rule.strip()
|
|
113
130
|
if rule == 'Bd':
|
|
114
|
-
solver_ls.append((
|
|
131
|
+
solver_ls.append(('Bd', lambda _: base_dir, 'Bd'))
|
|
115
132
|
continue
|
|
116
133
|
|
|
117
134
|
rule_solver = self.get_rule_solver(rule)
|
|
@@ -137,24 +154,14 @@ class DirRule:
|
|
|
137
154
|
|
|
138
155
|
@classmethod
|
|
139
156
|
def get_rule_solver(cls, rule: str) -> Optional[RuleSolver]:
|
|
140
|
-
# 查找缓存
|
|
141
|
-
if rule in cls.rule_solver_cache:
|
|
142
|
-
return cls.rule_solver_cache[rule]
|
|
143
|
-
|
|
144
157
|
# 检查dsl
|
|
145
158
|
if not rule.startswith(('A', 'P')):
|
|
146
159
|
return None
|
|
147
160
|
|
|
148
|
-
# Axxx or Pyyy
|
|
149
|
-
key = 1 if rule[0] == 'A' else 2
|
|
150
|
-
|
|
151
161
|
def solve_func(detail):
|
|
152
162
|
return fix_windir_name(str(DetailEntity.get_dirname(detail, rule[1:])))
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
rule_solver = (key, solve_func, rule)
|
|
156
|
-
cls.rule_solver_cache[rule] = rule_solver
|
|
157
|
-
return rule_solver
|
|
164
|
+
return rule[0], solve_func, rule
|
|
158
165
|
|
|
159
166
|
@classmethod
|
|
160
167
|
def apply_rule_solver(cls, album, photo, rule_solver: RuleSolver) -> str:
|
|
@@ -168,11 +175,11 @@ class DirRule:
|
|
|
168
175
|
"""
|
|
169
176
|
|
|
170
177
|
def choose_detail(key):
|
|
171
|
-
if key ==
|
|
178
|
+
if key == 'Bd':
|
|
172
179
|
return None
|
|
173
|
-
if key ==
|
|
180
|
+
if key == 'A':
|
|
174
181
|
return album
|
|
175
|
-
if key ==
|
|
182
|
+
if key == 'P':
|
|
176
183
|
return photo
|
|
177
184
|
|
|
178
185
|
key, func, _ = rule_solver
|
jmcomic/jm_plugin.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
该文件存放的是option插件
|
|
3
3
|
"""
|
|
4
|
+
import os.path
|
|
4
5
|
|
|
5
6
|
from .jm_option import *
|
|
6
7
|
|
|
@@ -296,91 +297,63 @@ class ZipPlugin(JmOptionPlugin):
|
|
|
296
297
|
zip_dir = JmcomicText.parse_to_abspath(zip_dir)
|
|
297
298
|
mkdir_if_not_exists(zip_dir)
|
|
298
299
|
|
|
299
|
-
|
|
300
|
-
dir_zip_dict: Dict[str, Optional[str]] = {}
|
|
300
|
+
path_to_delete = []
|
|
301
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)
|
|
305
|
-
self.zip_album(album, photo_dict, zip_path,
|
|
305
|
+
self.zip_album(album, photo_dict, zip_path, path_to_delete)
|
|
306
306
|
|
|
307
307
|
elif level == 'photo':
|
|
308
308
|
for photo, image_list in photo_dict.items():
|
|
309
309
|
zip_path = self.get_zip_path(None, photo, filename_rule, suffix, zip_dir)
|
|
310
|
-
self.zip_photo(photo, image_list, zip_path,
|
|
310
|
+
self.zip_photo(photo, image_list, zip_path, path_to_delete)
|
|
311
311
|
|
|
312
312
|
else:
|
|
313
313
|
ExceptionTool.raises(f'Not Implemented Zip Level: {level}')
|
|
314
314
|
|
|
315
|
-
self.after_zip(
|
|
315
|
+
self.after_zip(path_to_delete)
|
|
316
316
|
|
|
317
|
-
def zip_photo(self, photo, image_list: list, zip_path: str,
|
|
317
|
+
def zip_photo(self, photo, image_list: list, zip_path: str, path_to_delete):
|
|
318
318
|
"""
|
|
319
319
|
压缩photo文件夹
|
|
320
|
-
:returns: photo文件夹路径
|
|
321
320
|
"""
|
|
322
321
|
photo_dir = self.option.decide_image_save_dir(photo) \
|
|
323
322
|
if len(image_list) == 0 \
|
|
324
323
|
else os.path.dirname(image_list[0][0])
|
|
325
324
|
|
|
326
|
-
all_filepath = set(map(lambda t: self.unified_path(t[0]), image_list))
|
|
327
|
-
|
|
328
|
-
if len(all_filepath) == 0:
|
|
329
|
-
self.log('无下载文件,无需压缩', 'skip')
|
|
330
|
-
return None
|
|
331
|
-
|
|
332
325
|
from common import backup_dir_to_zip
|
|
333
|
-
backup_dir_to_zip(
|
|
334
|
-
photo_dir,
|
|
335
|
-
zip_path,
|
|
336
|
-
acceptor=lambda f: os.path.isdir(f) or self.unified_path(f) in all_filepath
|
|
337
|
-
).close()
|
|
326
|
+
backup_dir_to_zip(photo_dir, zip_path)
|
|
338
327
|
|
|
339
328
|
self.log(f'压缩章节[{photo.photo_id}]成功 → {zip_path}', 'finish')
|
|
340
|
-
|
|
329
|
+
path_to_delete.append(self.unified_path(photo_dir))
|
|
341
330
|
|
|
342
331
|
@staticmethod
|
|
343
332
|
def unified_path(f):
|
|
344
333
|
return fix_filepath(f, os.path.isdir(f))
|
|
345
334
|
|
|
346
|
-
def zip_album(self, album, photo_dict: dict, zip_path,
|
|
335
|
+
def zip_album(self, album, photo_dict: dict, zip_path, path_to_delete):
|
|
347
336
|
"""
|
|
348
337
|
压缩album文件夹
|
|
349
|
-
:returns: album文件夹路径
|
|
350
338
|
"""
|
|
351
339
|
|
|
352
|
-
|
|
353
|
-
all_filepath: Set[str] = set(path for ls in photo_dict.values() for path, _ in ls)
|
|
354
|
-
|
|
355
|
-
if len(all_filepath) == 0:
|
|
356
|
-
self.log('无下载文件,无需压缩', 'skip')
|
|
357
|
-
return
|
|
358
|
-
|
|
359
|
-
# 该本子的所有章节的图片所在文件夹
|
|
360
|
-
photo_dir_list = [self.option.decide_image_save_dir(photo) for photo in photo_dict.keys()]
|
|
361
|
-
|
|
362
|
-
# 压缩文件对象
|
|
363
|
-
from common import backup_dir_to_zip
|
|
340
|
+
album_dir = self.option.dir_rule.decide_album_root_dir(album)
|
|
364
341
|
import zipfile
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
photo_dir
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
)
|
|
376
|
-
dir_zip_dict[photo_dir] = zip_path
|
|
377
|
-
|
|
378
|
-
zfile.close()
|
|
342
|
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as f:
|
|
343
|
+
for photo in photo_dict.keys():
|
|
344
|
+
# 定位到章节所在文件夹
|
|
345
|
+
photo_dir = self.unified_path(self.option.decide_image_save_dir(photo))
|
|
346
|
+
# 章节文件夹标记为删除
|
|
347
|
+
path_to_delete.append(photo_dir)
|
|
348
|
+
for file in files_of_dir(photo_dir):
|
|
349
|
+
abspath = os.path.join(photo_dir, file)
|
|
350
|
+
relpath = os.path.relpath(abspath, album_dir)
|
|
351
|
+
f.write(abspath, relpath)
|
|
379
352
|
self.log(f'压缩本子[{album.album_id}]成功 → {zip_path}', 'finish')
|
|
380
353
|
|
|
381
|
-
def after_zip(self,
|
|
354
|
+
def after_zip(self, path_to_delete: List[str]):
|
|
382
355
|
# 删除所有原文件
|
|
383
|
-
dirs = sorted(
|
|
356
|
+
dirs = sorted(path_to_delete, reverse=True)
|
|
384
357
|
image_paths = [
|
|
385
358
|
path
|
|
386
359
|
for photo_dict in self.downloader.download_success_dict.values()
|
|
@@ -650,7 +623,7 @@ class FavoriteFolderExportPlugin(JmOptionPlugin):
|
|
|
650
623
|
"""
|
|
651
624
|
import zipfile
|
|
652
625
|
|
|
653
|
-
with zipfile.ZipFile(zip_path, 'w') as zipf:
|
|
626
|
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
654
627
|
# 获取文件夹中的文件列表并将其添加到 ZIP 文件中
|
|
655
628
|
for file in files:
|
|
656
629
|
zipf.write(file, arcname=of_file_name(file))
|
|
@@ -1035,3 +1008,62 @@ class SkipPhotoWithFewImagesPlugin(JmOptionPlugin):
|
|
|
1035
1008
|
@field_cache() # 单例
|
|
1036
1009
|
def build(cls, option: JmOption) -> 'JmOptionPlugin':
|
|
1037
1010
|
return super().build(option)
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
class DeleteDuplicatedFilesPlugin(JmOptionPlugin):
|
|
1014
|
+
"""
|
|
1015
|
+
https://github.com/hect0x7/JMComic-Crawler-Python/issues/244
|
|
1016
|
+
"""
|
|
1017
|
+
plugin_key = 'delete_duplicated_files'
|
|
1018
|
+
|
|
1019
|
+
@classmethod
|
|
1020
|
+
def calculate_md5(cls, file_path):
|
|
1021
|
+
import hashlib
|
|
1022
|
+
|
|
1023
|
+
"""计算文件的MD5哈希值"""
|
|
1024
|
+
hash_md5 = hashlib.md5()
|
|
1025
|
+
with open(file_path, "rb") as f:
|
|
1026
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
1027
|
+
hash_md5.update(chunk)
|
|
1028
|
+
return hash_md5.hexdigest()
|
|
1029
|
+
|
|
1030
|
+
@classmethod
|
|
1031
|
+
def find_duplicate_files(cls, root_folder):
|
|
1032
|
+
"""递归读取文件夹下所有文件并计算MD5出现次数"""
|
|
1033
|
+
import os
|
|
1034
|
+
from collections import defaultdict
|
|
1035
|
+
md5_dict = defaultdict(list)
|
|
1036
|
+
|
|
1037
|
+
for root, _, files in os.walk(root_folder):
|
|
1038
|
+
for file in files:
|
|
1039
|
+
file_path = os.path.join(root, file)
|
|
1040
|
+
file_md5 = cls.calculate_md5(file_path)
|
|
1041
|
+
md5_dict[file_md5].append(file_path)
|
|
1042
|
+
|
|
1043
|
+
return md5_dict
|
|
1044
|
+
|
|
1045
|
+
def invoke(self,
|
|
1046
|
+
limit,
|
|
1047
|
+
album=None,
|
|
1048
|
+
downloader=None,
|
|
1049
|
+
delete_original_file=True,
|
|
1050
|
+
**kwargs,
|
|
1051
|
+
) -> None:
|
|
1052
|
+
if album is None:
|
|
1053
|
+
return
|
|
1054
|
+
|
|
1055
|
+
self.delete_original_file = delete_original_file
|
|
1056
|
+
# 获取到下载本子所在根目录
|
|
1057
|
+
root_folder = self.option.dir_rule.decide_album_root_dir(album)
|
|
1058
|
+
self.find_duplicated_files_and_delete(limit, root_folder, album)
|
|
1059
|
+
|
|
1060
|
+
def find_duplicated_files_and_delete(self, limit: int, root_folder: str, album: Optional[JmAlbumDetail] = None):
|
|
1061
|
+
md5_dict = self.find_duplicate_files(root_folder)
|
|
1062
|
+
# 打印MD5出现次数大于等于limit的文件
|
|
1063
|
+
for md5, paths in md5_dict.items():
|
|
1064
|
+
if len(paths) >= limit:
|
|
1065
|
+
prefix = '' if album is None else f'({album.album_id}) '
|
|
1066
|
+
message = [prefix + f'MD5: {md5} 出现次数: {len(paths)}'] + \
|
|
1067
|
+
[f' {path}' for path in paths]
|
|
1068
|
+
self.log('\n'.join(message))
|
|
1069
|
+
self.execute_deletion(paths)
|
jmcomic/jm_toolkit.py
CHANGED
|
@@ -135,7 +135,7 @@ class JmcomicText:
|
|
|
135
135
|
last_pattern = pattern[len(pattern) - 1]
|
|
136
136
|
# 缩小文本
|
|
137
137
|
for i in range(0, len(pattern) - 1):
|
|
138
|
-
match = pattern[i].search(text)
|
|
138
|
+
match: Match = pattern[i].search(text)
|
|
139
139
|
if match is None:
|
|
140
140
|
return None
|
|
141
141
|
text = match[0]
|
|
@@ -707,7 +707,7 @@ class JmImageTool:
|
|
|
707
707
|
如果需要改变图片的文件格式,比如 .jpg → .png,则需要指定参数 neet_convert=True.
|
|
708
708
|
如果不需要改变图片的文件格式,使用 need_convert=False,可以跳过PIL解析图片,效率更高.
|
|
709
709
|
|
|
710
|
-
:param resp:
|
|
710
|
+
:param resp: JmImageResp
|
|
711
711
|
:param filepath: 图片文件路径
|
|
712
712
|
:param need_convert: 是否转换图片
|
|
713
713
|
"""
|
|
@@ -746,7 +746,7 @@ class JmImageTool:
|
|
|
746
746
|
|
|
747
747
|
# 无需解密,直接保存
|
|
748
748
|
if num == 0:
|
|
749
|
-
|
|
749
|
+
cls.save_image(img_src, decoded_save_path)
|
|
750
750
|
return
|
|
751
751
|
|
|
752
752
|
import math
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: jmcomic
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.14
|
|
4
4
|
Summary: Python API For JMComic (禁漫天堂)
|
|
5
5
|
Home-page: https://github.com/hect0x7/JMComic-Crawler-Python
|
|
6
6
|
Author: hect0x7
|
|
@@ -71,27 +71,50 @@ Requires-Dist: pycryptodome
|
|
|
71
71
|
|
|
72
72
|
## 快速上手
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
### 1. 下载本子方法
|
|
75
|
+
只需要使用如下代码,就可以下载本子`JM422866`的所有章节的图片:
|
|
76
76
|
```python
|
|
77
77
|
import jmcomic # 导入此模块,需要先安装.
|
|
78
78
|
jmcomic.download_album('422866') # 传入要下载的album的id,即可下载整个album到本地.
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
上面的 `download_album`方法还有一个参数`option`,可用于控制下载配置,配置包括禁漫域名、网络代理、图片格式转换、插件等等。
|
|
82
|
+
|
|
83
|
+
你可能需要这些配置项。推荐使用配置文件创建option,用option下载本子,见下章:
|
|
84
|
+
|
|
85
|
+
### 2. 使用option配置来下载本子
|
|
86
|
+
|
|
87
|
+
1. 首先,创建一个配置文件,假设文件名为 `option.yml`
|
|
88
|
+
|
|
89
|
+
该文件有特定的写法,你需要参考这个文档 → [option配置](./assets/docs/sources/option_file_syntax.md)
|
|
90
|
+
|
|
91
|
+
下面做一个演示,假设你需要把下载的图片转为png格式,你应该把以下内容写进`option.yml`
|
|
92
|
+
|
|
93
|
+
```yml
|
|
94
|
+
download:
|
|
95
|
+
image:
|
|
96
|
+
suffix: .png # 该配置用于把下载的图片转为png格式
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
2. 第二步,运行下面的python代码
|
|
100
|
+
```python
|
|
101
|
+
import jmcomic
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
# 创建配置对象
|
|
104
|
+
option = jmcomic.create_option_by_file('你的配置文件路径,例如 D:/option.yml')
|
|
105
|
+
# 使用option对象来下载本子
|
|
106
|
+
jmcomic.download_album(422866, option)
|
|
86
107
|
```
|
|
87
108
|
|
|
109
|
+
|
|
110
|
+
|
|
88
111
|
## 进阶使用
|
|
89
112
|
|
|
90
113
|
文档网站:[jmcomic.readthedocs.io](https://jmcomic.readthedocs.io/en/latest)
|
|
91
114
|
|
|
92
|
-
|
|
115
|
+
首先,就下载功能,jmcomic提供了很多配置项,大部分的下载需求你都可以通过上文介绍的配置文件来配置。
|
|
93
116
|
|
|
94
|
-
|
|
117
|
+
如果你不满足于下载,还有其他的使用需求,推荐你先看看以下文档:
|
|
95
118
|
|
|
96
119
|
* [jmcomic常用类和方法演示](assets/docs/sources/tutorial/0_demo.md)
|
|
97
120
|
* [option配置文件语法(包含插件配置)](./assets/docs/sources/option_file_syntax.md)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
jmcomic/__init__.py,sha256=M5LrJ_IvaYPMjPvydKmvbgueRZj0xVIzi2DQ6eppfBc,903
|
|
2
|
+
jmcomic/api.py,sha256=uLHtSof7ZaiWZcNDDAz6grD1NYs4VxsoCOrsge3G7v8,3864
|
|
3
|
+
jmcomic/cl.py,sha256=PBSh0JndNFZw3B7WJPj5Y8SeFdKzHE00jIwYo9An-K0,3475
|
|
4
|
+
jmcomic/jm_client_impl.py,sha256=Ab1b_l_NQSNp2__hjes6RRbMsOETmjlkf6O8ddNfHlg,38968
|
|
5
|
+
jmcomic/jm_client_interface.py,sha256=5JFSZEvAffDPh7oo8GQNVbm_Hdlmy_KYvTwrkY-OSA4,19074
|
|
6
|
+
jmcomic/jm_config.py,sha256=7Ms2SK5FdECJ5tolWXIRMzTlrgflgLX3b-3bL-Yq_38,16254
|
|
7
|
+
jmcomic/jm_downloader.py,sha256=0r4z7FRnow6xkRy_WTv7nLQOhYdtZmoouw0BNrhngco,10397
|
|
8
|
+
jmcomic/jm_entity.py,sha256=-WUKYGkNToQv5Hja3RkI5abHF1g46WpLyVjoyXMs-wI,18974
|
|
9
|
+
jmcomic/jm_exception.py,sha256=B9APE1jw23JBzCT12eV_imCZny3mNrSXju1p2IquaHA,4801
|
|
10
|
+
jmcomic/jm_option.py,sha256=4J7RJi1D2plVjNhPXE2QSjMuBeQzlpeAwRCojWqNqnw,22025
|
|
11
|
+
jmcomic/jm_plugin.py,sha256=xluJnzBugFMFUdz5yZ8Pb3BnIjKOECqIPjJD1rIod3Q,34911
|
|
12
|
+
jmcomic/jm_toolkit.py,sha256=hcg5qqpFlZilUVN2CmT2jtUK0ufRNCx7SuWYNT4BEdk,28662
|
|
13
|
+
jmcomic-2.5.14.dist-info/LICENSE,sha256=kz4coTxZxuGxisK3W00tjK57Zh3RcMGq-EnbXrK7-xA,1064
|
|
14
|
+
jmcomic-2.5.14.dist-info/METADATA,sha256=VO1j3KaCPYeTlXwDty6nOcVxn8YKgKjTzC5CRsFK0B8,7075
|
|
15
|
+
jmcomic-2.5.14.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
16
|
+
jmcomic-2.5.14.dist-info/entry_points.txt,sha256=tRbQltaGSBjejI0c9jYt-4SXQMd5nSDHcMvHmuTy4ow,44
|
|
17
|
+
jmcomic-2.5.14.dist-info/top_level.txt,sha256=puvVMFYJqIbd6NOTMEvOyugMTT8woBfSQyxEBan3zY4,8
|
|
18
|
+
jmcomic-2.5.14.dist-info/RECORD,,
|
jmcomic-2.5.11.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
jmcomic/__init__.py,sha256=Hjp1Q6tJ5zCFkSqh2ETXJuh_Rgpx5OKEjMhSbrU3dTs,903
|
|
2
|
-
jmcomic/api.py,sha256=uLHtSof7ZaiWZcNDDAz6grD1NYs4VxsoCOrsge3G7v8,3864
|
|
3
|
-
jmcomic/cl.py,sha256=PBSh0JndNFZw3B7WJPj5Y8SeFdKzHE00jIwYo9An-K0,3475
|
|
4
|
-
jmcomic/jm_client_impl.py,sha256=Oszkw7dKfg8DHzmjyBVD1DrHp9OlvLK33IQHB7uXo48,37811
|
|
5
|
-
jmcomic/jm_client_interface.py,sha256=vP36T2OI5MQdmfdYEnoFGRpHZRTE9LnBetDUFCOnMg4,17691
|
|
6
|
-
jmcomic/jm_config.py,sha256=I08aDW1WH9uVnbyiUvy4lfI8cAU8NNK96cEA5kGYcoM,15252
|
|
7
|
-
jmcomic/jm_downloader.py,sha256=0r4z7FRnow6xkRy_WTv7nLQOhYdtZmoouw0BNrhngco,10397
|
|
8
|
-
jmcomic/jm_entity.py,sha256=-WUKYGkNToQv5Hja3RkI5abHF1g46WpLyVjoyXMs-wI,18974
|
|
9
|
-
jmcomic/jm_exception.py,sha256=B9APE1jw23JBzCT12eV_imCZny3mNrSXju1p2IquaHA,4801
|
|
10
|
-
jmcomic/jm_option.py,sha256=s16w8uZYfCBUgsxCjegnLoAqHF69rJicVZKQTZlQTJA,21723
|
|
11
|
-
jmcomic/jm_plugin.py,sha256=21bd71tmsKyli73skMx0ozcqBM5saHzICEicjRQ98-Q,33606
|
|
12
|
-
jmcomic/jm_toolkit.py,sha256=WLHaigCN03Z4b5swGTRTTSWzZsa0uFxGAnw4xKhbh1U,28649
|
|
13
|
-
jmcomic-2.5.11.dist-info/LICENSE,sha256=kz4coTxZxuGxisK3W00tjK57Zh3RcMGq-EnbXrK7-xA,1064
|
|
14
|
-
jmcomic-2.5.11.dist-info/METADATA,sha256=2vGTgKwdQwztHQ8eIQ9cMOdHb5FTKYCPRZ_uWP6qP0E,6123
|
|
15
|
-
jmcomic-2.5.11.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
16
|
-
jmcomic-2.5.11.dist-info/entry_points.txt,sha256=tRbQltaGSBjejI0c9jYt-4SXQMd5nSDHcMvHmuTy4ow,44
|
|
17
|
-
jmcomic-2.5.11.dist-info/top_level.txt,sha256=puvVMFYJqIbd6NOTMEvOyugMTT8woBfSQyxEBan3zY4,8
|
|
18
|
-
jmcomic-2.5.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|