jmcomic 0.0.2__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/jm_config.py ADDED
@@ -0,0 +1,505 @@
1
+ from common import time_stamp, field_cache, ProxyBuilder
2
+
3
+
4
+ def shuffled(lines):
5
+ from random import shuffle
6
+ from common import str_to_list
7
+ ls = str_to_list(lines)
8
+ shuffle(ls)
9
+ return ls
10
+
11
+
12
+ def default_jm_logging(topic: str, msg: str):
13
+ from common import format_ts, current_thread
14
+ print('[{}] [{}]:【{}】{}'.format(format_ts(), current_thread().name, topic, msg))
15
+
16
+
17
+ # 禁漫常量
18
+ class JmMagicConstants:
19
+ # 搜索参数-排序
20
+ ORDER_BY_LATEST = 'mr'
21
+ ORDER_BY_VIEW = 'mv'
22
+ ORDER_BY_PICTURE = 'mp'
23
+ ORDER_BY_LIKE = 'tf'
24
+
25
+ ORDER_MONTH_RANKING = 'mv_m'
26
+ ORDER_WEEK_RANKING = 'mv_w'
27
+ ORDER_DAY_RANKING = 'mv_t'
28
+
29
+ # 搜索参数-时间段
30
+ TIME_TODAY = 't'
31
+ TIME_WEEK = 'w'
32
+ TIME_MONTH = 'm'
33
+ TIME_ALL = 'a'
34
+
35
+ # 分类参数API接口的category
36
+ CATEGORY_ALL = '0' # 全部
37
+ CATEGORY_DOUJIN = 'doujin' # 同人
38
+ CATEGORY_SINGLE = 'single' # 单本
39
+ CATEGORY_SHORT = 'short' # 短篇
40
+ CATEGORY_ANOTHER = 'another' # 其他
41
+ CATEGORY_HANMAN = 'hanman' # 韩漫
42
+ CATEGORY_MEIMAN = 'meiman' # 美漫
43
+ CATEGORY_DOUJIN_COSPLAY = 'doujin_cosplay' # cosplay
44
+ CATEGORY_3D = '3D' # 3D
45
+ CATEGORY_ENGLISH_SITE = 'english_site' # 英文站
46
+
47
+ # 副分类
48
+ SUB_CHINESE = 'chinese' # 汉化,通用副分类
49
+ SUB_JAPANESE = 'japanese' # 日语,通用副分类
50
+
51
+ # 其他类(CATEGORY_ANOTHER)的副分类
52
+ SUB_ANOTHER_OTHER = 'other' # 其他漫画
53
+ SUB_ANOTHER_3D = '3d' # 3D
54
+ SUB_ANOTHER_COSPLAY = 'cosplay' # cosplay
55
+
56
+ # 同人(SUB_CHINESE)的副分类
57
+ SUB_DOUJIN_CG = 'CG' # CG
58
+ SUB_DOUJIN_CHINESE = SUB_CHINESE
59
+ SUB_DOUJIN_JAPANESE = SUB_JAPANESE
60
+
61
+ # 短篇(CATEGORY_SHORT)的副分类
62
+ SUB_SHORT_CHINESE = SUB_CHINESE
63
+ SUB_SHORT_JAPANESE = SUB_JAPANESE
64
+
65
+ # 单本(CATEGORY_SINGLE)的副分类
66
+ SUB_SINGLE_CHINESE = SUB_CHINESE
67
+ SUB_SINGLE_JAPANESE = SUB_JAPANESE
68
+ SUB_SINGLE_YOUTH = 'youth'
69
+
70
+ # 图片分割参数
71
+ SCRAMBLE_220980 = 220980
72
+ SCRAMBLE_268850 = 268850
73
+ SCRAMBLE_421926 = 421926 # 2023-02-08后改了图片切割算法
74
+
75
+ # 移动端API密钥
76
+ APP_TOKEN_SECRET = '18comicAPP'
77
+ APP_TOKEN_SECRET_2 = '18comicAPPContent'
78
+ APP_DATA_SECRET = '185Hcomic3PAPP7R'
79
+ API_DOMAIN_SERVER_SECRET = 'diosfjckwpqpdfjkvnqQjsik'
80
+ APP_VERSION = '1.8.0'
81
+
82
+
83
+ # 模块级别共用配置
84
+ class JmModuleConfig:
85
+ # 网站相关
86
+ PROT = "https://"
87
+ JM_REDIRECT_URL = f'{PROT}jm365.work/3YeBdF' # 永久網域,怕走失的小伙伴收藏起来
88
+ JM_PUB_URL = f'{PROT}jmcomic-fb.vip'
89
+ JM_CDN_IMAGE_URL_TEMPLATE = PROT + 'cdn-msp.{domain}/media/photos/{photo_id}/{index:05}{suffix}' # index 从1开始
90
+ JM_IMAGE_SUFFIX = ['.jpg', '.webp', '.png', '.gif']
91
+
92
+ # JM的异常网页内容
93
+ JM_ERROR_RESPONSE_TEXT = {
94
+ "Could not connect to mysql! Please check your database settings!": "禁漫服务器内部报错",
95
+ "Restricted Access!": "禁漫拒绝你所在ip地区的访问,你可以选择: 换域名/换代理",
96
+ }
97
+
98
+ # JM的异常网页code
99
+ JM_ERROR_STATUS_CODE = {
100
+ 403: 'ip地区禁止访问/爬虫被识别',
101
+ 500: '500: 禁漫服务器内部异常(可能是服务器过载,可以切换ip或稍后重试)',
102
+ 520: '520: Web server is returning an unknown error (禁漫服务器内部报错)',
103
+ 524: '524: The origin web server timed out responding to this request. (禁漫服务器处理超时)',
104
+ }
105
+
106
+ # 分页大小
107
+ PAGE_SIZE_SEARCH = 80
108
+ PAGE_SIZE_FAVORITE = 20
109
+
110
+ # 图片分隔相关
111
+ SCRAMBLE_CACHE = {}
112
+
113
+ # 当本子没有作者名字时,顶替作者名字
114
+ DEFAULT_AUTHOR = 'default_author'
115
+
116
+ # cookies,目前只在移动端使用,因为移动端请求接口须携带,但不会校验cookies的内容。
117
+ APP_COOKIES = None
118
+
119
+ # 移动端图片域名
120
+ DOMAIN_IMAGE_LIST = shuffled('''
121
+ cdn-msp.jmapiproxy1.cc
122
+ cdn-msp.jmapiproxy2.cc
123
+ cdn-msp2.jmapiproxy2.cc
124
+ cdn-msp3.jmapiproxy2.cc
125
+ cdn-msp.jmapinodeudzn.net
126
+ cdn-msp3.jmapinodeudzn.net
127
+ ''')
128
+
129
+ # 移动端API域名
130
+ DOMAIN_API_LIST = shuffled('''
131
+ www.cdnmhwscc.vip
132
+ www.cdnplaystation6.club
133
+ www.cdnplaystation6.org
134
+ www.cdnuc.vip
135
+ www.cdn-mspjmapiproxy.xyz
136
+ ''')
137
+
138
+ # 获取最新移动端API域名的地址
139
+ API_URL_DOMAIN_SERVER = f'{PROT}jmappc01-1308024008.cos.ap-guangzhou.myqcloud.com/server-2024.txt'
140
+
141
+ APP_HEADERS_TEMPLATE = {
142
+ 'Accept-Encoding': 'gzip, deflate',
143
+ 'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.11211812; wv) AppleWebKit/537.36 (KHTML, '
144
+ 'like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36',
145
+ }
146
+
147
+ APP_HEADERS_IMAGE = {
148
+ 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
149
+ 'X-Requested-With': 'com.jiaohua_browser',
150
+ 'Referer': PROT + DOMAIN_API_LIST[0],
151
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
152
+ }
153
+
154
+ # 网页端headers
155
+ HTML_HEADERS_TEMPLATE = {
156
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
157
+ 'application/signed-exchange;v=b3;q=0.7',
158
+ 'accept-language': 'zh-CN,zh;q=0.9',
159
+ 'cache-control': 'no-cache',
160
+ 'dnt': '1',
161
+ 'pragma': 'no-cache',
162
+ 'priority': 'u=0, i',
163
+ 'referer': 'https://18comic.vip/',
164
+ 'sec-ch-ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
165
+ 'sec-ch-ua-mobile': '?0',
166
+ 'sec-ch-ua-platform': '"Windows"',
167
+ 'sec-fetch-dest': 'document',
168
+ 'sec-fetch-mode': 'navigate',
169
+ 'sec-fetch-site': 'none',
170
+ 'sec-fetch-user': '?1',
171
+ 'upgrade-insecure-requests': '1',
172
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 '
173
+ 'Safari/537.36',
174
+ }
175
+
176
+ # 网页端域名配置
177
+ # 无需配置,默认为None,需要的时候会发起请求获得
178
+ # 使用优先级:
179
+ # 1. DOMAIN_HTML_LIST
180
+ # 2. [DOMAIN_HTML]
181
+ DOMAIN_HTML = None
182
+ DOMAIN_HTML_LIST = None
183
+
184
+ # 模块级别的可重写类配置
185
+ CLASS_DOWNLOADER = None
186
+ CLASS_OPTION = None
187
+ CLASS_ALBUM = None
188
+ CLASS_PHOTO = None
189
+ CLASS_IMAGE = None
190
+
191
+ # 客户端注册表
192
+ REGISTRY_CLIENT = {}
193
+ # 插件注册表
194
+ REGISTRY_PLUGIN = {}
195
+ # 异常监听器
196
+ # key: 异常类
197
+ # value: 函数,参数只有异常对象,无需返回值
198
+ # 这个异常类(或者这个异常的子类)的实例将要被raise前,你的listener方法会被调用
199
+ REGISTRY_EXCEPTION_LISTENER = {}
200
+
201
+ # 执行log的函数
202
+ EXECUTOR_LOG = default_jm_logging
203
+
204
+ # 使用固定时间戳
205
+ FLAG_USE_FIX_TIMESTAMP = True
206
+ # 移动端Client初始化cookies
207
+ FLAG_API_CLIENT_REQUIRE_COOKIES = True
208
+ # 自动更新禁漫API域名
209
+ FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN = True
210
+ FLAG_API_CLIENT_AUTO_UPDATE_DOMAIN_DONE = None
211
+ # log开关标记
212
+ FLAG_ENABLE_JM_LOG = True
213
+ # log时解码url
214
+ FLAG_DECODE_URL_WHEN_LOGGING = True
215
+ # 当内置的版本号落后时,使用最新的禁漫app版本号
216
+ FLAG_USE_VERSION_NEWER_IF_BEHIND = True
217
+
218
+ # 关联dir_rule的自定义字段与对应的处理函数
219
+ # 例如:
220
+ # Amyname -> JmModuleConfig.AFIELD_ADVICE['myname'] = lambda album: "自定义名称"
221
+ AFIELD_ADVICE = dict()
222
+ PFIELD_ADVICE = dict()
223
+
224
+ # 当发生 oserror: [Errno 36] File name too long 时,
225
+ # 把文件名限制在指定个字符以内
226
+ VAR_FILE_NAME_LENGTH_LIMIT = 100
227
+
228
+ @classmethod
229
+ def downloader_class(cls):
230
+ if cls.CLASS_DOWNLOADER is not None:
231
+ return cls.CLASS_DOWNLOADER
232
+
233
+ from .jm_downloader import JmDownloader
234
+ return JmDownloader
235
+
236
+ @classmethod
237
+ def option_class(cls):
238
+ if cls.CLASS_OPTION is not None:
239
+ return cls.CLASS_OPTION
240
+
241
+ from .jm_option import JmOption
242
+ return JmOption
243
+
244
+ @classmethod
245
+ def album_class(cls):
246
+ if cls.CLASS_ALBUM is not None:
247
+ return cls.CLASS_ALBUM
248
+
249
+ from .jm_entity import JmAlbumDetail
250
+ return JmAlbumDetail
251
+
252
+ @classmethod
253
+ def photo_class(cls):
254
+ if cls.CLASS_PHOTO is not None:
255
+ return cls.CLASS_PHOTO
256
+
257
+ from .jm_entity import JmPhotoDetail
258
+ return JmPhotoDetail
259
+
260
+ @classmethod
261
+ def image_class(cls):
262
+ if cls.CLASS_IMAGE is not None:
263
+ return cls.CLASS_IMAGE
264
+
265
+ from .jm_entity import JmImageDetail
266
+ return JmImageDetail
267
+
268
+ @classmethod
269
+ def client_impl_class(cls, client_key: str):
270
+ clazz_dict = cls.REGISTRY_CLIENT
271
+
272
+ clazz = clazz_dict.get(client_key, None)
273
+ if clazz is None:
274
+ from .jm_toolkit import ExceptionTool
275
+ ExceptionTool.raises(f'not found client impl class for key: "{client_key}"')
276
+
277
+ return clazz
278
+
279
+ @classmethod
280
+ @field_cache("DOMAIN_HTML")
281
+ def get_html_domain(cls, postman=None):
282
+ """
283
+ 由于禁漫的域名经常变化,调用此方法可以获取一个当前可用的最新的域名 domain,
284
+ 并且设置把 domain 设置为禁漫模块的默认域名。
285
+ 这样一来,配置文件也不用配置域名了,一切都在运行时动态获取。
286
+ """
287
+ from .jm_toolkit import JmcomicText
288
+ return JmcomicText.parse_to_jm_domain(cls.get_html_url(postman))
289
+
290
+ @classmethod
291
+ def get_html_url(cls, postman=None):
292
+ """
293
+ 访问禁漫的永久网域,从而得到一个可用的禁漫网址
294
+ :returns: https://jm-comic2.cc
295
+ """
296
+ postman = postman or cls.new_postman(session=True)
297
+
298
+ url = postman.with_redirect_catching().get(cls.JM_REDIRECT_URL)
299
+ cls.jm_log('module.html_url', f'获取禁漫网页URL: [{cls.JM_REDIRECT_URL}] → [{url}]')
300
+ return url
301
+
302
+ @classmethod
303
+ @field_cache("DOMAIN_HTML_LIST")
304
+ def get_html_domain_all(cls, postman=None):
305
+ """
306
+ 访问禁漫发布页,得到所有的禁漫网页域名
307
+
308
+ :returns: ['18comic.vip', ..., 'jm365.xyz/ZNPJam'], 最后一个是【APP軟件下載】
309
+ """
310
+ postman = postman or cls.new_postman(session=True)
311
+
312
+ resp = postman.get(cls.JM_PUB_URL)
313
+ if resp.status_code != 200:
314
+ from .jm_toolkit import ExceptionTool
315
+ ExceptionTool.raises_resp(f'请求失败,访问禁漫发布页获取所有域名,HTTP状态码为: {resp.status_code}', resp)
316
+
317
+ from .jm_toolkit import JmcomicText
318
+ domain_list = JmcomicText.analyse_jm_pub_html(resp.text)
319
+
320
+ cls.jm_log('module.html_domain_all', f'获取禁漫网页全部域名: [{resp.url}] → {domain_list}')
321
+ return domain_list
322
+
323
+ @classmethod
324
+ def get_html_domain_all_via_github(cls,
325
+ postman=None,
326
+ template='https://jmcmomic.github.io/go/{}.html',
327
+ index_range=(300, 309)
328
+ ):
329
+ """
330
+ 通过禁漫官方的github号的repo获取最新的禁漫域名
331
+ https://github.com/jmcmomic/jmcmomic.github.io
332
+ """
333
+ postman = postman or cls.new_postman(headers={
334
+ 'authority': 'github.com',
335
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 '
336
+ 'Safari/537.36'
337
+ })
338
+ domain_set = set()
339
+
340
+ def fetch_domain(url):
341
+ resp = postman.get(url, allow_redirects=False)
342
+ text = resp.text
343
+ from .jm_toolkit import JmcomicText
344
+ for domain in JmcomicText.analyse_jm_pub_html(text):
345
+ if domain.startswith('jm365'):
346
+ continue
347
+ domain_set.add(domain)
348
+
349
+ from common import multi_thread_launcher
350
+
351
+ multi_thread_launcher(
352
+ iter_objs=[template.format(i) for i in range(*index_range)],
353
+ apply_each_obj_func=fetch_domain,
354
+ )
355
+
356
+ return domain_set
357
+
358
+ @classmethod
359
+ def new_html_headers(cls, domain='18comic.vip'):
360
+ """
361
+ 网页端的headers
362
+ """
363
+ headers = cls.HTML_HEADERS_TEMPLATE.copy()
364
+ headers.update({
365
+ 'authority': domain,
366
+ 'origin': f'https://{domain}',
367
+ 'referer': f'https://{domain}',
368
+ })
369
+ return headers
370
+
371
+ @classmethod
372
+ @field_cache()
373
+ def get_fix_ts_token_tokenparam(cls):
374
+ ts = time_stamp()
375
+ from .jm_toolkit import JmCryptoTool
376
+ token, tokenparam = JmCryptoTool.token_and_tokenparam(ts)
377
+ return ts, token, tokenparam
378
+
379
+ # noinspection PyUnusedLocal
380
+ @classmethod
381
+ def jm_log(cls, topic: str, msg: str):
382
+ if cls.FLAG_ENABLE_JM_LOG is True:
383
+ cls.EXECUTOR_LOG(topic, msg)
384
+
385
+ @classmethod
386
+ def disable_jm_log(cls):
387
+ cls.FLAG_ENABLE_JM_LOG = False
388
+
389
+ @classmethod
390
+ def new_postman(cls, session=False, **kwargs):
391
+ kwargs.setdefault('impersonate', 'chrome')
392
+ kwargs.setdefault('headers', JmModuleConfig.new_html_headers())
393
+ kwargs.setdefault('proxies', JmModuleConfig.DEFAULT_PROXIES)
394
+
395
+ from common import Postmans
396
+
397
+ if session is True:
398
+ return Postmans.new_session(**kwargs)
399
+
400
+ return Postmans.new_postman(**kwargs)
401
+
402
+ # option 相关的默认配置
403
+ # 一般情况下,建议使用option配置文件来定制配置
404
+ # 而如果只想修改几个简单常用的配置,也可以下方的DEFAULT_XXX属性
405
+ JM_OPTION_VER = '2.1'
406
+ DEFAULT_CLIENT_IMPL = 'api' # 默认Client实现类型为网页端
407
+ DEFAULT_CLIENT_CACHE = None # 默认关闭Client缓存。缓存的配置详见 CacheRegistry
408
+ DEFAULT_PROXIES = ProxyBuilder.system_proxy() # 默认使用系统代理
409
+
410
+ DEFAULT_OPTION_DICT: dict = {
411
+ 'log': None,
412
+ 'dir_rule': {'rule': 'Bd_Pname', 'base_dir': None},
413
+ 'download': {
414
+ 'cache': True,
415
+ 'image': {'decode': True, 'suffix': None},
416
+ 'threading': {
417
+ 'image': 30,
418
+ 'photo': None,
419
+ },
420
+ },
421
+ 'client': {
422
+ 'cache': None, # see CacheRegistry
423
+ 'domain': [],
424
+ 'postman': {
425
+ 'type': 'curl_cffi',
426
+ 'meta_data': {
427
+ 'impersonate': 'chrome',
428
+ 'headers': None,
429
+ 'proxies': None,
430
+ }
431
+ },
432
+ 'impl': None,
433
+ 'retry_times': 5,
434
+ },
435
+ 'plugins': {
436
+ # 如果插件抛出参数校验异常,只log。(全局配置,可以被插件的局部配置覆盖)
437
+ # 可选值:ignore(忽略),log(打印日志),raise(抛异常)。
438
+ 'valid': 'log',
439
+ },
440
+ }
441
+
442
+ @classmethod
443
+ def option_default_dict(cls) -> dict:
444
+ """
445
+ 返回JmOption.default()的默认配置字典。
446
+ 这样做是为了支持外界自行覆盖option默认配置字典
447
+ """
448
+ from copy import deepcopy
449
+
450
+ option_dict = deepcopy(cls.DEFAULT_OPTION_DICT)
451
+
452
+ # log
453
+ if option_dict['log'] is None:
454
+ option_dict['log'] = cls.FLAG_ENABLE_JM_LOG
455
+
456
+ # dir_rule.base_dir
457
+ dir_rule = option_dict['dir_rule']
458
+ if dir_rule['base_dir'] is None:
459
+ import os
460
+ dir_rule['base_dir'] = os.getcwd()
461
+
462
+ # client cache
463
+ client = option_dict['client']
464
+ if client['cache'] is None:
465
+ client['cache'] = cls.DEFAULT_CLIENT_CACHE
466
+
467
+ # client impl
468
+ if client['impl'] is None:
469
+ client['impl'] = cls.DEFAULT_CLIENT_IMPL
470
+
471
+ # postman proxies
472
+ meta_data = client['postman']['meta_data']
473
+ if meta_data['proxies'] is None:
474
+ # use system proxy by default
475
+ meta_data['proxies'] = cls.DEFAULT_PROXIES
476
+
477
+ # threading photo
478
+ dt = option_dict['download']['threading']
479
+ if dt['photo'] is None:
480
+ import os
481
+ dt['photo'] = os.cpu_count()
482
+
483
+ return option_dict
484
+
485
+ @classmethod
486
+ def register_plugin(cls, plugin_class):
487
+ from .jm_toolkit import ExceptionTool
488
+ ExceptionTool.require_true(getattr(plugin_class, 'plugin_key', None) is not None,
489
+ f'未配置plugin_key, class: {plugin_class}')
490
+ cls.REGISTRY_PLUGIN[plugin_class.plugin_key] = plugin_class
491
+
492
+ @classmethod
493
+ def register_client(cls, client_class):
494
+ from .jm_toolkit import ExceptionTool
495
+ ExceptionTool.require_true(getattr(client_class, 'client_key', None) is not None,
496
+ f'未配置client_key, class: {client_class}')
497
+ cls.REGISTRY_CLIENT[client_class.client_key] = client_class
498
+
499
+ @classmethod
500
+ def register_exception_listener(cls, etype, listener):
501
+ cls.REGISTRY_EXCEPTION_LISTENER[etype] = listener
502
+
503
+
504
+ jm_log = JmModuleConfig.jm_log
505
+ disable_jm_log = JmModuleConfig.disable_jm_log