novel-downloader 1.1.1__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.
Files changed (109) hide show
  1. novel_downloader/__init__.py +14 -0
  2. novel_downloader/cli/__init__.py +14 -0
  3. novel_downloader/cli/clean.py +134 -0
  4. novel_downloader/cli/download.py +98 -0
  5. novel_downloader/cli/interactive.py +67 -0
  6. novel_downloader/cli/main.py +45 -0
  7. novel_downloader/cli/settings.py +177 -0
  8. novel_downloader/config/__init__.py +52 -0
  9. novel_downloader/config/adapter.py +150 -0
  10. novel_downloader/config/loader.py +177 -0
  11. novel_downloader/config/models.py +170 -0
  12. novel_downloader/config/site_rules.py +97 -0
  13. novel_downloader/core/__init__.py +25 -0
  14. novel_downloader/core/downloaders/__init__.py +20 -0
  15. novel_downloader/core/downloaders/base_downloader.py +187 -0
  16. novel_downloader/core/downloaders/common_downloader.py +192 -0
  17. novel_downloader/core/downloaders/qidian_downloader.py +208 -0
  18. novel_downloader/core/factory/__init__.py +21 -0
  19. novel_downloader/core/factory/downloader_factory.py +62 -0
  20. novel_downloader/core/factory/parser_factory.py +62 -0
  21. novel_downloader/core/factory/requester_factory.py +62 -0
  22. novel_downloader/core/factory/saver_factory.py +49 -0
  23. novel_downloader/core/interfaces/__init__.py +28 -0
  24. novel_downloader/core/interfaces/downloader_protocol.py +37 -0
  25. novel_downloader/core/interfaces/parser_protocol.py +40 -0
  26. novel_downloader/core/interfaces/requester_protocol.py +65 -0
  27. novel_downloader/core/interfaces/saver_protocol.py +61 -0
  28. novel_downloader/core/parsers/__init__.py +28 -0
  29. novel_downloader/core/parsers/base_parser.py +96 -0
  30. novel_downloader/core/parsers/common_parser/__init__.py +14 -0
  31. novel_downloader/core/parsers/common_parser/helper.py +321 -0
  32. novel_downloader/core/parsers/common_parser/main_parser.py +86 -0
  33. novel_downloader/core/parsers/qidian_parser/__init__.py +20 -0
  34. novel_downloader/core/parsers/qidian_parser/browser/__init__.py +13 -0
  35. novel_downloader/core/parsers/qidian_parser/browser/chapter_encrypted.py +498 -0
  36. novel_downloader/core/parsers/qidian_parser/browser/chapter_normal.py +97 -0
  37. novel_downloader/core/parsers/qidian_parser/browser/chapter_router.py +70 -0
  38. novel_downloader/core/parsers/qidian_parser/browser/main_parser.py +110 -0
  39. novel_downloader/core/parsers/qidian_parser/session/__init__.py +13 -0
  40. novel_downloader/core/parsers/qidian_parser/session/chapter_encrypted.py +451 -0
  41. novel_downloader/core/parsers/qidian_parser/session/chapter_normal.py +119 -0
  42. novel_downloader/core/parsers/qidian_parser/session/chapter_router.py +67 -0
  43. novel_downloader/core/parsers/qidian_parser/session/main_parser.py +113 -0
  44. novel_downloader/core/parsers/qidian_parser/session/node_decryptor.py +164 -0
  45. novel_downloader/core/parsers/qidian_parser/shared/__init__.py +38 -0
  46. novel_downloader/core/parsers/qidian_parser/shared/book_info_parser.py +95 -0
  47. novel_downloader/core/parsers/qidian_parser/shared/helpers.py +133 -0
  48. novel_downloader/core/requesters/__init__.py +27 -0
  49. novel_downloader/core/requesters/base_browser.py +210 -0
  50. novel_downloader/core/requesters/base_session.py +243 -0
  51. novel_downloader/core/requesters/common_requester/__init__.py +14 -0
  52. novel_downloader/core/requesters/common_requester/common_session.py +126 -0
  53. novel_downloader/core/requesters/qidian_requester/__init__.py +22 -0
  54. novel_downloader/core/requesters/qidian_requester/qidian_broswer.py +377 -0
  55. novel_downloader/core/requesters/qidian_requester/qidian_session.py +202 -0
  56. novel_downloader/core/savers/__init__.py +20 -0
  57. novel_downloader/core/savers/base_saver.py +169 -0
  58. novel_downloader/core/savers/common_saver/__init__.py +13 -0
  59. novel_downloader/core/savers/common_saver/common_epub.py +232 -0
  60. novel_downloader/core/savers/common_saver/common_txt.py +176 -0
  61. novel_downloader/core/savers/common_saver/main_saver.py +86 -0
  62. novel_downloader/core/savers/epub_utils/__init__.py +27 -0
  63. novel_downloader/core/savers/epub_utils/css_builder.py +68 -0
  64. novel_downloader/core/savers/epub_utils/initializer.py +98 -0
  65. novel_downloader/core/savers/epub_utils/text_to_html.py +132 -0
  66. novel_downloader/core/savers/epub_utils/volume_intro.py +61 -0
  67. novel_downloader/core/savers/qidian_saver.py +22 -0
  68. novel_downloader/locales/en.json +91 -0
  69. novel_downloader/locales/zh.json +91 -0
  70. novel_downloader/resources/config/rules.toml +196 -0
  71. novel_downloader/resources/config/settings.yaml +70 -0
  72. novel_downloader/resources/css_styles/main.css +104 -0
  73. novel_downloader/resources/css_styles/volume-intro.css +56 -0
  74. novel_downloader/resources/images/volume_border.png +0 -0
  75. novel_downloader/resources/js_scripts/qidian_decrypt_node.js +82 -0
  76. novel_downloader/resources/json/replace_word_map.json +4 -0
  77. novel_downloader/resources/text/blacklist.txt +22 -0
  78. novel_downloader/utils/__init__.py +0 -0
  79. novel_downloader/utils/cache.py +24 -0
  80. novel_downloader/utils/constants.py +158 -0
  81. novel_downloader/utils/crypto_utils.py +144 -0
  82. novel_downloader/utils/file_utils/__init__.py +43 -0
  83. novel_downloader/utils/file_utils/io.py +252 -0
  84. novel_downloader/utils/file_utils/normalize.py +68 -0
  85. novel_downloader/utils/file_utils/sanitize.py +77 -0
  86. novel_downloader/utils/fontocr/__init__.py +23 -0
  87. novel_downloader/utils/fontocr/ocr_v1.py +304 -0
  88. novel_downloader/utils/fontocr/ocr_v2.py +658 -0
  89. novel_downloader/utils/hash_store.py +288 -0
  90. novel_downloader/utils/hash_utils.py +103 -0
  91. novel_downloader/utils/i18n.py +41 -0
  92. novel_downloader/utils/logger.py +104 -0
  93. novel_downloader/utils/model_loader.py +72 -0
  94. novel_downloader/utils/network.py +287 -0
  95. novel_downloader/utils/state.py +156 -0
  96. novel_downloader/utils/text_utils/__init__.py +27 -0
  97. novel_downloader/utils/text_utils/chapter_formatting.py +46 -0
  98. novel_downloader/utils/text_utils/diff_display.py +75 -0
  99. novel_downloader/utils/text_utils/font_mapping.py +31 -0
  100. novel_downloader/utils/text_utils/text_cleaning.py +57 -0
  101. novel_downloader/utils/time_utils/__init__.py +22 -0
  102. novel_downloader/utils/time_utils/datetime_utils.py +146 -0
  103. novel_downloader/utils/time_utils/sleep_utils.py +49 -0
  104. novel_downloader-1.1.1.dist-info/METADATA +137 -0
  105. novel_downloader-1.1.1.dist-info/RECORD +109 -0
  106. novel_downloader-1.1.1.dist-info/WHEEL +5 -0
  107. novel_downloader-1.1.1.dist-info/entry_points.txt +2 -0
  108. novel_downloader-1.1.1.dist-info/licenses/LICENSE +21 -0
  109. novel_downloader-1.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,196 @@
1
+ [qidian.profile] # 起点
2
+ book_info_url = "https://book.qidian.com/info/{book_id}/"
3
+ chapter_url = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
4
+
5
+ [qidian.book_info.book_name]
6
+ steps = [
7
+ { type = "select_one", selector = "em#bookName" },
8
+ { type = "text" },
9
+ { type = "strip" }
10
+ ]
11
+
12
+ [qidian.book_info.author]
13
+ steps = [
14
+ { type = "select_one", selector = "a.writer" },
15
+ { type = "text" },
16
+ { type = "strip" }
17
+ ]
18
+
19
+ [qidian.book_info.cover_url]
20
+ steps = [
21
+ { type = "select_one", selector = "div.book-img img" },
22
+ { type = "attr", attr = "src" },
23
+ { type = "strip" }
24
+ ]
25
+
26
+ [qidian.book_info.update_time]
27
+ steps = [
28
+ { type = "select_one", selector = "span.book-update-time" },
29
+ { type = "text" },
30
+ { type = "replace", old = "更新时间", new = "" },
31
+ { type = "strip" }
32
+ ]
33
+
34
+ [qidian.book_info.serial_status]
35
+ steps = [
36
+ { type = "select_one", selector = "span.blue" },
37
+ { type = "text" },
38
+ { type = "strip" }
39
+ ]
40
+
41
+ [qidian.book_info.word_count]
42
+ steps = [
43
+ { type = "regex", pattern = "<em>([\\d.]+)</em>\\s*<cite>(.*?)字</cite>", template = "$1$2字" }
44
+ ]
45
+
46
+ [qidian.book_info.summary]
47
+ steps = [
48
+ { type = "select_one", selector = "div.book-intro p" },
49
+ { type = "text" },
50
+ { type = "strip" }
51
+ ]
52
+
53
+ [qidian.book_info.volumes]
54
+ has_volume = true
55
+ volume_selector = "div.volume-wrap div.volume"
56
+ chapter_selector = "li"
57
+
58
+ volume_name_steps = [
59
+ { type = "select_one", selector = "h3" },
60
+ { type = "text" },
61
+ { type = "split", sep = "·", index = 0 },
62
+ { type = "replace", old = "订阅本卷", new = "" },
63
+ { type = "strip" }
64
+ ]
65
+
66
+ chapter_steps = [
67
+ { key = "title", steps = [
68
+ { type = "select_one", selector = "a" },
69
+ { type = "text" }
70
+ ] },
71
+
72
+ { key = "url", steps = [
73
+ { type = "select_one", selector = "a" },
74
+ { type = "attr", attr = "href" }
75
+ ] },
76
+
77
+ { key = "chapterId", steps = [
78
+ { type = "select_one", selector = "a" },
79
+ { type = "attr", attr = "href" },
80
+ { type = "regex", pattern = "/(\\d+)", group = 1 }
81
+ ] }
82
+ ]
83
+
84
+ [qidian.chapter.title] # 章节标题
85
+ steps = [
86
+ { type = "select_one", selector = "div#app div.print h1.title" },
87
+ { type = "exclude", selector = "span.review" },
88
+ { type = "text" },
89
+ { type = "strip" }
90
+ ]
91
+
92
+ [qidian.chapter.content] # 章节正文
93
+ steps = [
94
+ { type = "select_one", selector = "div#app div#reader-content main" },
95
+ { type = "exclude", selector = "span.review" },
96
+ { type = "find_all", name = "p" },
97
+ { type = "text" },
98
+ { type = "strip" },
99
+ { type = "join", sep = "\n\n" }
100
+ ]
101
+
102
+ [biquge.profile] # 笔趣阁
103
+ book_info_url = "http://www.b520.cc/{book_id}/"
104
+ chapter_url = "http://www.b520.cc/{book_id}/{chapter_id}.html"
105
+
106
+ [biquge.book_info.book_name]
107
+ steps = [
108
+ { type = "select_one", selector = "div#info > h1" },
109
+ { type = "text" },
110
+ { type = "strip" }
111
+ ]
112
+
113
+ [biquge.book_info.author]
114
+ steps = [
115
+ { type = "select", selector = "div#info > p", index = 0 },
116
+ { type = "text" },
117
+ { type = "replace", old = "\u00a0", new = "" },
118
+ { type = "regex", pattern = "作\\s*者[::]?\\s*(\\S+)", group = 1 },
119
+ { type = "strip" }
120
+ ]
121
+
122
+ [biquge.book_info.cover_url]
123
+ steps = [
124
+ { type = "select_one", selector = "div#fmimg > img" },
125
+ { type = "attr", attr = "src" },
126
+ { type = "strip" }
127
+ ]
128
+
129
+ [biquge.book_info.update_time]
130
+ steps = [
131
+ { type = "select", selector = "div#info > p", index = 2 },
132
+ { type = "text" },
133
+ { type = "regex", pattern = "最后更新[::]\\s*(\\S+)", group = 1 },
134
+ { type = "strip" }
135
+ ]
136
+
137
+ # [biquge.book_info.serial_status] # 没有
138
+
139
+ # [biquge.book_info.word_count] # 没有
140
+
141
+ [biquge.book_info.summary]
142
+ steps = [
143
+ { type = "select_one", selector = "div#intro" },
144
+ { type = "text" },
145
+ { type = "strip" }
146
+ ]
147
+
148
+ [biquge.book_info.volumes]
149
+ has_volume = true
150
+ volume_mode = "mixed"
151
+ list_selector = "div#list"
152
+ volume_selector = "dt"
153
+ chapter_selector = "dd"
154
+
155
+ volume_name_steps = [
156
+ { type = "select_one", selector = "dt" },
157
+ { type = "text" },
158
+ { type = "strip" },
159
+ { type = "split", sep = "\n", index = 0 },
160
+ { type = "strip" }
161
+ ]
162
+
163
+ chapter_steps = [
164
+ { key = "title", steps = [
165
+ { type = "select_one", selector = "a" },
166
+ { type = "text" }
167
+ ] },
168
+
169
+ { key = "url", steps = [
170
+ { type = "select_one", selector = "a" },
171
+ { type = "attr", attr = "href" }
172
+ ] },
173
+
174
+ { key = "chapterId", steps = [
175
+ { type = "select_one", selector = "a" },
176
+ { type = "attr", attr = "href" },
177
+ { type = "replace", old = ".html", new = "" },
178
+ { type = "regex", pattern = ".*/(\\d+)$", group = 1 }
179
+ ] }
180
+ ]
181
+
182
+ [biquge.chapter.title] # 章节标题
183
+ steps = [
184
+ { type = "select_one", selector = "div.bookname > h1" },
185
+ { type = "text" },
186
+ { type = "strip" }
187
+ ]
188
+
189
+ [biquge.chapter.content] # 章节正文
190
+ steps = [
191
+ { type = "select_one", selector = "div#content" },
192
+ { type = "find_all", name = "p" },
193
+ { type = "text" },
194
+ { type = "strip" },
195
+ { type = "join", sep = "\n\n" }
196
+ ]
@@ -0,0 +1,70 @@
1
+ # 网络请求层设置
2
+ requests:
3
+ wait_time: 5 # 每次请求等待时间 (秒)
4
+ retry_times: 3 # 请求失败重试次数
5
+ retry_interval: 5
6
+ timeout: 30 # 页面加载超时时间 (秒)
7
+ # DrissionPage 专用设置
8
+ headless: false # 是否以无头模式启动浏览器
9
+ user_data_folder: "" # 浏览器用户数据目录: 为空则使用默认目录
10
+ profile_name: "" # 使用的用户配置名称: 为空则使用默认配置
11
+ auto_close: true # 页面抓取完是否自动关闭浏览器
12
+ disable_images: false # 是否禁用图片加载 (加速)
13
+ mute_audio: true # 是否静音
14
+
15
+ # 全局通用设置
16
+ general:
17
+ request_interval: 5 # 同一本书各章节请求间隔 (秒)
18
+ raw_data_dir: "./raw_data" # 原始章节 HTML/JSON 存放目录
19
+ output_dir: "./downloads" # 最终输出文件存放目录
20
+ cache_dir: "./novel_cache" # 本地缓存目录 (字体 / 图片等)
21
+ max_threads: 4 # 最大并发下载线程数 (未实现)
22
+ skip_existing: true # 是否跳过已存在章节
23
+ debug:
24
+ save_html: false # 是否将抓取到的原始 HTML 保留到磁盘
25
+ log_level: "INFO" # 日志级别: DEBUG, INFO, WARNING, ERROR
26
+
27
+ # 各站点的特定配置
28
+ sites:
29
+ qidian:
30
+ # 小说 ID 列表
31
+ # 例如: 访问 https://www.qidian.com/book/1010868264/
32
+ # 该小说的 ID 就是 1010868264
33
+ book_ids:
34
+ - "0000000000"
35
+ - "0000000000"
36
+ mode: "browser" # browser / session
37
+ login_required: true # 是否需要登录才能访问
38
+ decode_font: false # 是否尝试本地解码混淆字体
39
+ use_freq: false # 是否使用频率分析
40
+ ocr_version: "v2.0" # "v1.0" / "v2.0"
41
+ use_ocr: true # 是否使用 OCR 辅助识别文本
42
+ use_vec: false # 是否使用 Vector 辅助识别文本
43
+ save_font_debug: false # 是否保存字体解码调试数据
44
+ batch_size: 32
45
+ ocr_weight: 0.6
46
+ vec_weight: 0.4
47
+ #
48
+ sample_site:
49
+ book_ids:
50
+ - "ABCDEF"
51
+ api_key: "your_api_key"
52
+ special_flag: 42
53
+ common:
54
+ mode: "session" # async / session
55
+ login_required: false # 是否需要登录才能访问
56
+
57
+ # 输出文件格式及相关选项
58
+ output:
59
+ clean_text: true # 是否对章节文本做清理
60
+ formats:
61
+ make_txt: true # 是否生成完整 TXT 文件
62
+ make_epub: false # 是否生成 EPUB
63
+ make_md: false # 是否生成 Markdown (未实现)
64
+ make_pdf: false # 可能支持 PDF 输出 (未实现)
65
+ naming:
66
+ append_timestamp: false # 在文件名中追加时间戳
67
+ filename_template: "{title}_{author}" # 文件命名规则
68
+ epub:
69
+ include_cover: true # 是否在 EPUB 中包含封面
70
+ include_toc: false # 是否自动生成目录
@@ -0,0 +1,104 @@
1
+ html, body {
2
+ height: 95%;
3
+ padding: 0.5em;
4
+ }
5
+
6
+ .images img {
7
+ width:100%; /*自适应*/
8
+ }
9
+ .color .images {
10
+ margin:0 0 1.5em;
11
+ }
12
+ .article .images {
13
+ margin:1.0em 0;
14
+ border:1px solid #000
15
+ }
16
+
17
+ .new-page-after {
18
+ page-break-after:always;
19
+ }
20
+
21
+ .cover {
22
+ width:100%;
23
+ padding:0px;
24
+ }
25
+ .center {
26
+ text-align: center;
27
+ margin-left: 0%;
28
+ margin-right: 0%;
29
+ }
30
+ .left {
31
+ text-align: center;
32
+ margin-left: 0%;
33
+ margin-right: 0%;
34
+ }
35
+ .right {
36
+ text-align: right;
37
+ margin-left: 0%;
38
+ margin-right: 0%;
39
+ }
40
+
41
+ hr {
42
+ margin: 0;
43
+ border-style: dotted;
44
+ border-width: 1px 0 0 0;
45
+ margin-top: 0.5em;
46
+ margin-bottom: 0.5em;
47
+ }
48
+
49
+ h1 {
50
+ text-indent: 0;
51
+ duokan-text-indent: 0;
52
+ line-height: 110%;
53
+ text-align: center;
54
+ font-size: 1.2em;
55
+ margin-top: 0.4em;
56
+ margin-bottom: 0.4em;
57
+ }
58
+
59
+ h2, h3, h4, h5, h6 {
60
+ text-indent: 0;
61
+ duokan-text-indent: 0;
62
+ line-height: 110%;
63
+ font-size: 1.1em;
64
+ margin-top: 0.4em;
65
+ margin-bottom: 0.4em;
66
+ }
67
+
68
+ p {
69
+ text-align: justify; /* 让文本两端对齐 */
70
+ text-indent: 2em;
71
+ duokan-text-indent: 2em;
72
+ line-height: 100%;
73
+ margin-top: 0.4em;
74
+ margin-bottom: 0.4em;
75
+ }
76
+
77
+ div.list {
78
+ text-align: left;
79
+ font-size: 0.85em;
80
+ line-height: 130%;
81
+ }
82
+
83
+ span {
84
+ text-indent: 0;
85
+ duokan-text-indent: 0;
86
+ text-align: left;
87
+ font-size: 1em;
88
+ }
89
+
90
+ ol {
91
+ text-align: left;
92
+ margin-top: 1.2em;
93
+ margin-bottom: 1.2em;
94
+ margin-left: 0;
95
+ list-style-type: none;
96
+ }
97
+
98
+ li {
99
+ text-align: left;
100
+ margin-top: 0.4em;
101
+ margin-bottom: 0.4em;
102
+ margin-left: 0;
103
+ list-style-type: none;
104
+ }
@@ -0,0 +1,56 @@
1
+ /*标题页*/
2
+
3
+ h1.volume-title-line1 {
4
+ font-size: 1.25rem; /* 第一行较大 */
5
+ font-weight: bold;
6
+ text-align: center;
7
+ text-indent: 0em;
8
+ duokan-text-indent: 0em;
9
+ color: #6e471c;
10
+ text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8);
11
+ margin: 0;
12
+ display: block;
13
+ }
14
+
15
+ p.volume-title-line2 {
16
+ font-size: 1rem; /* 第二行稍小 */
17
+ font-weight: bold;
18
+ text-align: center;
19
+ text-indent: 0em;
20
+ duokan-text-indent: 0em;
21
+ color: #6e471c;
22
+ text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8);
23
+ margin: 0;
24
+ display: block;
25
+ }
26
+
27
+ div.border1 {
28
+ margin: 35% auto 0 auto;
29
+ text-align: center;
30
+ text-indent: 0em;
31
+ duokan-text-indent: 0em;
32
+ }
33
+
34
+ div.border2 {
35
+ margin: 0 auto 0 auto;
36
+ text-align: center;
37
+ text-indent: 0em;
38
+ duokan-text-indent: 0em;
39
+ transform: rotate(180deg);
40
+ -ms-transform: rotate(180deg);
41
+ /* IE 9 */
42
+ -moz-transform: rotate(180deg);
43
+ /* Firefox */
44
+ -webkit-transform: rotate(180deg);
45
+ /* Safari 和 Chrome */
46
+ -o-transform: rotate(180deg);
47
+ /* Opera */;
48
+ }
49
+
50
+ img.border1 {
51
+ width: 100%;
52
+ }
53
+
54
+ img.border2 {
55
+ width: 80%;
56
+ }
@@ -0,0 +1,82 @@
1
+ window = global;
2
+ null_fun = function(){console.log(arguments);}
3
+ window.outerHeight = 1000
4
+ window.innerHeight = 100
5
+ globalThis = window
6
+ self = window
7
+ window.location = {}
8
+ location.protocol = "https:"
9
+ location.hostname = "vipreader.qidian.com"
10
+ setTimeout = null_fun
11
+ setInterval = null_fun
12
+ document = {createElement: null_fun, documentElement: {}, createEvent: null_fun, currentScript: {src: "https://qdfepccdn.qidian.com/www.qidian.com/fock/116594983210.js"}, domain: 'qidian.com'}
13
+ navigator = {userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'}
14
+ performance = {}
15
+ performance.navigation = {type: 1}
16
+
17
+ // ---- DECRYPT FUNCTION ----
18
+ const Fock = require('./4819793b.qeooxh.js');
19
+
20
+ async function decrypt(enContent, cuChapterId, fkp, fuid) {
21
+ Fock.initialize();
22
+ Fock.setupUserKey(fuid);
23
+ eval(atob(fkp));
24
+ isFockInit = true;
25
+
26
+ return new Promise((resolve, reject) => {
27
+ Fock.unlock(enContent, cuChapterId, (code, decrypted) => {
28
+ if (code === 0) {
29
+ resolve(decrypted);
30
+ } else {
31
+ reject(new Error(`Fock.unlock failed, code=${code}`));
32
+ }
33
+ });
34
+ });
35
+ }
36
+
37
+ // ---- MAIN ----
38
+ const fs = require('fs');
39
+
40
+ (async () => {
41
+ const [inputPath, outputPath] = process.argv.slice(2);
42
+ if (!inputPath || !outputPath) {
43
+ console.error(
44
+ "Usage: node script.js <input.json> <output.txt>"
45
+ );
46
+ process.exit(1);
47
+ }
48
+
49
+ try {
50
+ const inputData = fs.readFileSync(inputPath, "utf-8");
51
+ const [
52
+ raw_enContent,
53
+ raw_cuChapterId,
54
+ raw_fkp,
55
+ raw_fuid
56
+ ] = JSON.parse(inputData);
57
+
58
+ const decryptPromise = decrypt(
59
+ String(raw_enContent),
60
+ String(raw_cuChapterId),
61
+ String(raw_fkp),
62
+ String(raw_fuid)
63
+ );
64
+
65
+ const timeoutMs = 5000;
66
+ const timerPromise = new Promise((_, reject) => {
67
+ setTimeout(() => {
68
+ reject(new Error(`decrypt timeout after ${timeoutMs}ms`));
69
+ }, timeoutMs);
70
+ });
71
+
72
+ const result = await Promise.race([
73
+ decryptPromise,
74
+ timerPromise
75
+ ]);
76
+
77
+ fs.writeFileSync(outputPath, result, "utf-8");
78
+ } catch (err) {
79
+ console.error("Failed to decrypt:", err);
80
+ process.exit(1);
81
+ }
82
+ })();
@@ -0,0 +1,4 @@
1
+ {
2
+ "source_1": "target_1",
3
+ "source_2": "target_2"
4
+ }
@@ -0,0 +1,22 @@
1
+ 求票
2
+ 月票
3
+ 投票
4
+ 首订
5
+ 订阅
6
+ 追订
7
+ 追读
8
+ 加更
9
+ 推荐
10
+ 收藏
11
+ 新书
12
+ 成绩
13
+ 建议
14
+ 太监
15
+ 烂尾
16
+ 完本
17
+ 更新
18
+ 支持
19
+ 感谢
20
+ 推书
21
+ 盟主
22
+ 提示
File without changes
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ novel_downloader.utils.cache
5
+ ----------------------------
6
+
7
+ Provides decorators for caching function results,
8
+ specifically optimized for configuration loading functions.
9
+ """
10
+
11
+ from functools import lru_cache, wraps
12
+ from typing import Any, Callable, TypeVar, cast
13
+
14
+ T = TypeVar("T", bound=Callable[..., Any])
15
+
16
+
17
+ def cached_load_config(func: T) -> T:
18
+ """
19
+ A decorator to cache the result of a config-loading function.
20
+ Uses LRU cache with maxsize=1.
21
+ """
22
+ cached = lru_cache(maxsize=1)(func)
23
+ wrapped = wraps(func)(cached)
24
+ return cast(T, wrapped)