novel-downloader 2.0.1__py3-none-any.whl → 2.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.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/download.py +11 -8
- novel_downloader/cli/export.py +17 -17
- novel_downloader/cli/ui.py +28 -1
- novel_downloader/config/adapter.py +27 -1
- novel_downloader/core/archived/deqixs/fetcher.py +1 -28
- novel_downloader/core/downloaders/__init__.py +2 -0
- novel_downloader/core/downloaders/base.py +34 -85
- novel_downloader/core/downloaders/common.py +147 -171
- novel_downloader/core/downloaders/qianbi.py +30 -64
- novel_downloader/core/downloaders/qidian.py +157 -184
- novel_downloader/core/downloaders/qqbook.py +292 -0
- novel_downloader/core/downloaders/registry.py +2 -2
- novel_downloader/core/exporters/__init__.py +2 -0
- novel_downloader/core/exporters/base.py +37 -59
- novel_downloader/core/exporters/common.py +620 -0
- novel_downloader/core/exporters/linovelib.py +47 -0
- novel_downloader/core/exporters/qidian.py +41 -12
- novel_downloader/core/exporters/qqbook.py +28 -0
- novel_downloader/core/exporters/registry.py +2 -2
- novel_downloader/core/fetchers/__init__.py +4 -2
- novel_downloader/core/fetchers/aaatxt.py +2 -22
- novel_downloader/core/fetchers/b520.py +3 -23
- novel_downloader/core/fetchers/base.py +80 -105
- novel_downloader/core/fetchers/biquyuedu.py +2 -22
- novel_downloader/core/fetchers/dxmwx.py +10 -22
- novel_downloader/core/fetchers/esjzone.py +6 -29
- novel_downloader/core/fetchers/guidaye.py +2 -22
- novel_downloader/core/fetchers/hetushu.py +9 -29
- novel_downloader/core/fetchers/i25zw.py +2 -16
- novel_downloader/core/fetchers/ixdzs8.py +2 -16
- novel_downloader/core/fetchers/jpxs123.py +2 -16
- novel_downloader/core/fetchers/lewenn.py +2 -22
- novel_downloader/core/fetchers/linovelib.py +4 -20
- novel_downloader/core/fetchers/{eightnovel.py → n8novel.py} +12 -40
- novel_downloader/core/fetchers/piaotia.py +2 -16
- novel_downloader/core/fetchers/qbtr.py +2 -16
- novel_downloader/core/fetchers/qianbi.py +1 -20
- novel_downloader/core/fetchers/qidian.py +7 -33
- novel_downloader/core/fetchers/qqbook.py +177 -0
- novel_downloader/core/fetchers/quanben5.py +9 -29
- novel_downloader/core/fetchers/rate_limiter.py +22 -53
- novel_downloader/core/fetchers/sfacg.py +3 -16
- novel_downloader/core/fetchers/shencou.py +2 -16
- novel_downloader/core/fetchers/shuhaige.py +2 -22
- novel_downloader/core/fetchers/tongrenquan.py +2 -22
- novel_downloader/core/fetchers/ttkan.py +3 -14
- novel_downloader/core/fetchers/wanbengo.py +2 -22
- novel_downloader/core/fetchers/xiaoshuowu.py +2 -16
- novel_downloader/core/fetchers/xiguashuwu.py +4 -20
- novel_downloader/core/fetchers/xs63b.py +3 -15
- novel_downloader/core/fetchers/xshbook.py +2 -22
- novel_downloader/core/fetchers/yamibo.py +4 -28
- novel_downloader/core/fetchers/yibige.py +13 -26
- novel_downloader/core/interfaces/exporter.py +19 -7
- novel_downloader/core/interfaces/fetcher.py +21 -47
- novel_downloader/core/parsers/__init__.py +4 -2
- novel_downloader/core/parsers/b520.py +2 -2
- novel_downloader/core/parsers/base.py +4 -39
- novel_downloader/core/parsers/{eightnovel.py → n8novel.py} +5 -5
- novel_downloader/core/parsers/{qidian/main_parser.py → qidian.py} +147 -266
- novel_downloader/core/parsers/qqbook.py +709 -0
- novel_downloader/core/parsers/xiguashuwu.py +3 -4
- novel_downloader/core/searchers/__init__.py +2 -2
- novel_downloader/core/searchers/b520.py +1 -1
- novel_downloader/core/searchers/base.py +2 -2
- novel_downloader/core/searchers/{eightnovel.py → n8novel.py} +5 -5
- novel_downloader/models/__init__.py +2 -0
- novel_downloader/models/book.py +1 -0
- novel_downloader/models/config.py +12 -0
- novel_downloader/resources/config/settings.toml +23 -5
- novel_downloader/resources/js_scripts/expr_to_json.js +14 -0
- novel_downloader/resources/js_scripts/qidian_decrypt_node.js +21 -16
- novel_downloader/resources/js_scripts/qq_decrypt_node.js +92 -0
- novel_downloader/utils/constants.py +6 -0
- novel_downloader/utils/crypto_utils/aes_util.py +1 -1
- novel_downloader/utils/epub/constants.py +1 -6
- novel_downloader/utils/fontocr/core.py +2 -0
- novel_downloader/utils/fontocr/loader.py +10 -8
- novel_downloader/utils/node_decryptor/__init__.py +13 -0
- novel_downloader/utils/node_decryptor/decryptor.py +342 -0
- novel_downloader/{core/parsers/qidian/utils → utils/node_decryptor}/decryptor_fetcher.py +5 -6
- novel_downloader/web/pages/download.py +1 -1
- novel_downloader/web/pages/search.py +1 -1
- novel_downloader/web/services/task_manager.py +2 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/METADATA +4 -1
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/RECORD +91 -94
- novel_downloader/core/exporters/common/__init__.py +0 -11
- novel_downloader/core/exporters/common/epub.py +0 -198
- novel_downloader/core/exporters/common/main_exporter.py +0 -64
- novel_downloader/core/exporters/common/txt.py +0 -146
- novel_downloader/core/exporters/epub_util.py +0 -215
- novel_downloader/core/exporters/linovelib/__init__.py +0 -11
- novel_downloader/core/exporters/linovelib/epub.py +0 -349
- novel_downloader/core/exporters/linovelib/main_exporter.py +0 -66
- novel_downloader/core/exporters/linovelib/txt.py +0 -139
- novel_downloader/core/exporters/txt_util.py +0 -67
- novel_downloader/core/parsers/qidian/__init__.py +0 -10
- novel_downloader/core/parsers/qidian/utils/__init__.py +0 -11
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +0 -175
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/WHEEL +0 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/entry_points.txt +0 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/top_level.txt +0 -0
@@ -284,8 +284,7 @@ class XiguashuwuParser(BaseParser):
|
|
284
284
|
return char
|
285
285
|
return f'<img src="{url}" />'
|
286
286
|
|
287
|
-
|
288
|
-
def _recognize_glyph_from_url(cls, url: str) -> str | None:
|
287
|
+
def _recognize_glyph_from_url(self, url: str) -> str | None:
|
289
288
|
"""
|
290
289
|
Download the glyph image at `url` and run the font OCR on it.
|
291
290
|
|
@@ -293,7 +292,7 @@ class XiguashuwuParser(BaseParser):
|
|
293
292
|
:return: The recognized character (top-1) if OCR succeeds, otherwise None.
|
294
293
|
"""
|
295
294
|
try:
|
296
|
-
ocr = get_font_ocr()
|
295
|
+
ocr = get_font_ocr(self._fontocr_cfg)
|
297
296
|
if not ocr:
|
298
297
|
return None
|
299
298
|
|
@@ -304,7 +303,7 @@ class XiguashuwuParser(BaseParser):
|
|
304
303
|
|
305
304
|
char, score = ocr.predict([img_np])[0]
|
306
305
|
|
307
|
-
return char if score >=
|
306
|
+
return char if score >= self._CONF_THRESHOLD else None
|
308
307
|
|
309
308
|
except Exception as e:
|
310
309
|
logger.warning("[Parser] Failed to ocr glyph image %s: %s", url, e)
|
@@ -11,12 +11,12 @@ __all__ = [
|
|
11
11
|
"AaatxtSearcher",
|
12
12
|
"BiqugeSearcher",
|
13
13
|
"DxmwxSearcher",
|
14
|
-
"EightnovelSearcher",
|
15
14
|
"EsjzoneSearcher",
|
16
15
|
"HetushuSearcher",
|
17
16
|
"I25zwSearcher",
|
18
17
|
"Ixdzs8Searcher",
|
19
18
|
"Jpxs123Searcher",
|
19
|
+
"N8novelSearcher",
|
20
20
|
"PiaotiaSearcher",
|
21
21
|
"QbtrSearcher",
|
22
22
|
"QianbiSearcher",
|
@@ -32,12 +32,12 @@ __all__ = [
|
|
32
32
|
from .aaatxt import AaatxtSearcher
|
33
33
|
from .b520 import BiqugeSearcher
|
34
34
|
from .dxmwx import DxmwxSearcher
|
35
|
-
from .eightnovel import EightnovelSearcher
|
36
35
|
from .esjzone import EsjzoneSearcher
|
37
36
|
from .hetushu import HetushuSearcher
|
38
37
|
from .i25zw import I25zwSearcher
|
39
38
|
from .ixdzs8 import Ixdzs8Searcher
|
40
39
|
from .jpxs123 import Jpxs123Searcher
|
40
|
+
from .n8novel import N8novelSearcher
|
41
41
|
from .piaotia import PiaotiaSearcher
|
42
42
|
from .qbtr import QbtrSearcher
|
43
43
|
from .qianbi import QianbiSearcher
|
@@ -12,13 +12,13 @@ from urllib.parse import quote_plus, urljoin
|
|
12
12
|
|
13
13
|
import aiohttp
|
14
14
|
|
15
|
-
from novel_downloader.core.interfaces import SearcherProtocol
|
16
15
|
from novel_downloader.models import SearchResult
|
17
16
|
from novel_downloader.utils.constants import DEFAULT_USER_HEADERS
|
18
17
|
|
19
18
|
|
20
|
-
class BaseSearcher(abc.ABC
|
19
|
+
class BaseSearcher(abc.ABC):
|
21
20
|
site_name: str
|
21
|
+
priority: int = 1000
|
22
22
|
BASE_URL: str = ""
|
23
23
|
_session: ClassVar[aiohttp.ClientSession | None] = None
|
24
24
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.searchers.
|
4
|
-
|
3
|
+
novel_downloader.core.searchers.n8novel
|
4
|
+
---------------------------------------
|
5
5
|
|
6
6
|
"""
|
7
7
|
|
@@ -17,10 +17,10 @@ logger = logging.getLogger(__name__)
|
|
17
17
|
|
18
18
|
|
19
19
|
@register_searcher(
|
20
|
-
site_keys=["
|
20
|
+
site_keys=["n8novel", "8novel"],
|
21
21
|
)
|
22
|
-
class
|
23
|
-
site_name = "
|
22
|
+
class N8novelSearcher(BaseSearcher):
|
23
|
+
site_name = "n8novel"
|
24
24
|
priority = 20
|
25
25
|
BASE_URL = "https://www.8novel.com"
|
26
26
|
SEARCH_URL = "https://www.8novel.com/search/"
|
@@ -11,6 +11,7 @@ __all__ = [
|
|
11
11
|
"DownloaderConfig",
|
12
12
|
"ParserConfig",
|
13
13
|
"FetcherConfig",
|
14
|
+
"FontOCRConfig",
|
14
15
|
"ExporterConfig",
|
15
16
|
"TextCleanerConfig",
|
16
17
|
"BookInfoDict",
|
@@ -32,6 +33,7 @@ from .config import (
|
|
32
33
|
DownloaderConfig,
|
33
34
|
ExporterConfig,
|
34
35
|
FetcherConfig,
|
36
|
+
FontOCRConfig,
|
35
37
|
ParserConfig,
|
36
38
|
TextCleanerConfig,
|
37
39
|
)
|
novel_downloader/models/book.py
CHANGED
@@ -39,6 +39,17 @@ class DownloaderConfig:
|
|
39
39
|
storage_batch_size: int = 1
|
40
40
|
|
41
41
|
|
42
|
+
@dataclass
|
43
|
+
class FontOCRConfig:
|
44
|
+
model_name: str | None = None
|
45
|
+
model_dir: str | None = None
|
46
|
+
input_shape: tuple[int, int, int] | None = None
|
47
|
+
device: str | None = None
|
48
|
+
precision: str = "fp32"
|
49
|
+
cpu_threads: int = 10
|
50
|
+
enable_hpi: bool = False
|
51
|
+
|
52
|
+
|
42
53
|
@dataclass
|
43
54
|
class ParserConfig:
|
44
55
|
cache_dir: str = "./novel_cache"
|
@@ -46,6 +57,7 @@ class ParserConfig:
|
|
46
57
|
decode_font: bool = False
|
47
58
|
batch_size: int = 32
|
48
59
|
save_font_debug: bool = False
|
60
|
+
fontocr_cfg: FontOCRConfig = field(default_factory=FontOCRConfig)
|
49
61
|
|
50
62
|
|
51
63
|
@dataclass
|
@@ -2,13 +2,13 @@
|
|
2
2
|
[general]
|
3
3
|
retry_times = 3 # 请求失败重试次数
|
4
4
|
backoff_factor = 2.0
|
5
|
-
timeout = 30.0 #
|
5
|
+
timeout = 30.0 # 请求加载超时时间 (秒)
|
6
6
|
max_connections = 10 # 并发连接的最大数
|
7
|
-
max_rps =
|
8
|
-
request_interval =
|
9
|
-
raw_data_dir = "./raw_data" #
|
7
|
+
max_rps = 1000.0 # 最大请求速率 (requests per second), 如不设置则请设置一个较大的数或负数
|
8
|
+
request_interval = 1.0 # 同一本书各章节请求间隔 (秒)
|
9
|
+
raw_data_dir = "./raw_data" # 书籍原始 JSON/DB/图片 数据存放目录
|
10
10
|
output_dir = "./downloads" # 最终输出文件存放目录
|
11
|
-
cache_dir = "./novel_cache" # 本地缓存目录
|
11
|
+
cache_dir = "./novel_cache" # 本地缓存目录
|
12
12
|
workers = 2 # 工作协程数
|
13
13
|
skip_existing = true # 是否跳过已存在章节
|
14
14
|
storage_batch_size = 1 # SQLite 批量提交的章节数量
|
@@ -23,6 +23,17 @@ decode_font = false # 是否尝试本地解码混淆字体
|
|
23
23
|
batch_size = 32
|
24
24
|
save_font_debug = false # 是否保存字体解码调试数据
|
25
25
|
|
26
|
+
# 以下为 PaddleOCR 文本识别的可选参数
|
27
|
+
# 默认情况下, 请保持注释, 程序会使用 PaddleOCR 内置的默认值
|
28
|
+
# 如果需要修改配置, 请取消注释并填写参数值 (参考官网文档)
|
29
|
+
# https://www.paddleocr.ai/main/version3.x/module_usage/text_recognition.html
|
30
|
+
|
31
|
+
model_name = "PP-OCRv5_mobile_rec"
|
32
|
+
# model_dir = "/path/to/inference"
|
33
|
+
# input_shape = [3, 48, 320] # [C, H, W], 按所选模型文档填写, 不确定请注释
|
34
|
+
# device = "gpu" # "cpu" / "gpu"
|
35
|
+
# cpu_threads = 10
|
36
|
+
|
26
37
|
# 各站点的特定配置
|
27
38
|
[sites.qidian] # 起点中文网
|
28
39
|
book_ids = [
|
@@ -32,6 +43,13 @@ book_ids = [
|
|
32
43
|
login_required = true # 是否需要登录才能访问
|
33
44
|
use_truncation = true # 是否基于章节长度截断以避免重复内容
|
34
45
|
|
46
|
+
[sites.qqbook] # QQ 阅读
|
47
|
+
book_ids = [
|
48
|
+
"0000000000",
|
49
|
+
"0000000000"
|
50
|
+
]
|
51
|
+
login_required = true
|
52
|
+
|
35
53
|
[sites.esjzone] # ESJ Zone
|
36
54
|
book_ids = [
|
37
55
|
"0000000000",
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
let code = "";
|
4
|
+
process.stdin.on("data", chunk => code += chunk);
|
5
|
+
process.stdin.on("end", () => {
|
6
|
+
try {
|
7
|
+
// Make sure object literals parse correctly
|
8
|
+
const result = eval("(" + code + ")");
|
9
|
+
console.log(JSON.stringify(result));
|
10
|
+
} catch (err) {
|
11
|
+
console.error("Error:", err);
|
12
|
+
process.exit(1);
|
13
|
+
}
|
14
|
+
});
|
@@ -1,18 +1,22 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
// ---- GLOBAL ENVIRONMENT SHIMS ----
|
4
|
+
const shimEnv = {
|
5
|
+
outerHeight: 1000,
|
6
|
+
innerHeight: 100,
|
7
|
+
location: {
|
8
|
+
protocol: "https:",
|
9
|
+
hostname: "vipreader.qidian.com",
|
10
|
+
},
|
11
|
+
};
|
12
|
+
|
13
|
+
global.window = global;
|
14
|
+
globalThis.window = global;
|
15
|
+
globalThis.self = global;
|
16
|
+
|
17
|
+
for (const [key, value] of Object.entries(shimEnv)) {
|
18
|
+
globalThis[key] = value;
|
19
|
+
}
|
16
20
|
|
17
21
|
// ---- DECRYPT FUNCTION ----
|
18
22
|
const Fock = require('./4819793b.qeooxh.js');
|
@@ -41,7 +45,7 @@ const fs = require('fs');
|
|
41
45
|
const [inputPath, outputPath] = process.argv.slice(2);
|
42
46
|
if (!inputPath || !outputPath) {
|
43
47
|
console.error(
|
44
|
-
"Usage: node
|
48
|
+
"Usage: node decrypt_qd.js <input.json> <output.txt>"
|
45
49
|
);
|
46
50
|
process.exit(1);
|
47
51
|
}
|
@@ -75,6 +79,7 @@ const fs = require('fs');
|
|
75
79
|
]);
|
76
80
|
|
77
81
|
fs.writeFileSync(outputPath, result, "utf-8");
|
82
|
+
process.exit(0);
|
78
83
|
} catch (err) {
|
79
84
|
console.error("Failed to decrypt:", err);
|
80
85
|
process.exit(1);
|
@@ -0,0 +1,92 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
// ---- GLOBAL ENVIRONMENT SHIMS ----
|
4
|
+
const _setInterval = global.setInterval;
|
5
|
+
|
6
|
+
const shimEnv = {
|
7
|
+
location: {
|
8
|
+
protocol: "https:",
|
9
|
+
hostname: "book.qq.com",
|
10
|
+
},
|
11
|
+
setInterval: (fn, t) => typeof fn === "function" ? _setInterval(fn, t) : undefined,
|
12
|
+
document: {
|
13
|
+
createElement: (tag) => {
|
14
|
+
if (tag === "iframe") {
|
15
|
+
const win = {};
|
16
|
+
win.window = win;
|
17
|
+
win.eval = (code) => eval(code);
|
18
|
+
return {
|
19
|
+
style: {},
|
20
|
+
contentWindow: win,
|
21
|
+
};
|
22
|
+
}
|
23
|
+
return { style: {}, appendChild: () => {} };
|
24
|
+
},
|
25
|
+
body: { appendChild: () => {} },
|
26
|
+
},
|
27
|
+
};
|
28
|
+
|
29
|
+
global.window = global;
|
30
|
+
globalThis.window = global;
|
31
|
+
globalThis.self = global;
|
32
|
+
|
33
|
+
for (const [key, value] of Object.entries(shimEnv)) {
|
34
|
+
globalThis[key] = value;
|
35
|
+
}
|
36
|
+
|
37
|
+
// ---- DECRYPT FUNCTION ----
|
38
|
+
const Fock = require('./cefc2a5d.pz1phw.js');
|
39
|
+
|
40
|
+
async function decrypt(enContent, cuChapterId, fkp, fuid) {
|
41
|
+
Fock.setupUserKey(fuid);
|
42
|
+
eval(atob(fkp));
|
43
|
+
isFockInit = true;
|
44
|
+
|
45
|
+
return new Promise((resolve, reject) => {
|
46
|
+
Fock.unlock(enContent, cuChapterId, (code, decrypted) => {
|
47
|
+
if (code === 0) {
|
48
|
+
resolve(decrypted);
|
49
|
+
} else {
|
50
|
+
reject(new Error(`Fock.unlock failed, code=${code}`));
|
51
|
+
}
|
52
|
+
});
|
53
|
+
});
|
54
|
+
}
|
55
|
+
|
56
|
+
// ---- MAIN ----
|
57
|
+
const fs = require('fs');
|
58
|
+
|
59
|
+
(async () => {
|
60
|
+
const [inputPath, outputPath] = process.argv.slice(2);
|
61
|
+
|
62
|
+
if (!inputPath || !outputPath) {
|
63
|
+
console.error("Usage: node decrypt_qq.js <input.json> <output.txt>");
|
64
|
+
process.exit(1);
|
65
|
+
}
|
66
|
+
|
67
|
+
try {
|
68
|
+
const inputData = fs.readFileSync(inputPath, "utf-8");
|
69
|
+
const [raw_enContent, raw_cuChapterId, raw_fkp, raw_fuid] = JSON.parse(inputData);
|
70
|
+
|
71
|
+
const decryptPromise = decrypt(
|
72
|
+
String(raw_enContent),
|
73
|
+
String(raw_cuChapterId),
|
74
|
+
String(raw_fkp),
|
75
|
+
String(raw_fuid)
|
76
|
+
);
|
77
|
+
|
78
|
+
const timeoutMs = 5000;
|
79
|
+
const timerPromise = new Promise((_, reject) => {
|
80
|
+
setTimeout(() => reject(new Error(`decrypt timeout after ${timeoutMs}ms`)), timeoutMs);
|
81
|
+
});
|
82
|
+
|
83
|
+
const result = await Promise.race([decryptPromise, timerPromise]);
|
84
|
+
console.log("result", result);
|
85
|
+
|
86
|
+
fs.writeFileSync(outputPath, result, "utf-8");
|
87
|
+
process.exit(0);
|
88
|
+
} catch (err) {
|
89
|
+
console.error("Failed to decrypt:", err);
|
90
|
+
process.exit(1);
|
91
|
+
}
|
92
|
+
})();
|
@@ -90,6 +90,12 @@ XIGUASHUWU_FONT_MAP_PATH = files("novel_downloader.resources.json").joinpath(
|
|
90
90
|
)
|
91
91
|
|
92
92
|
# JavaScript
|
93
|
+
EXPR_TO_JSON_SCRIPT_PATH = files("novel_downloader.resources.js_scripts").joinpath(
|
94
|
+
"expr_to_json.js"
|
95
|
+
)
|
93
96
|
QD_DECRYPT_SCRIPT_PATH = files("novel_downloader.resources.js_scripts").joinpath(
|
94
97
|
"qidian_decrypt_node.js"
|
95
98
|
)
|
99
|
+
QQ_DECRYPT_SCRIPT_PATH = files("novel_downloader.resources.js_scripts").joinpath(
|
100
|
+
"qq_decrypt_node.js"
|
101
|
+
)
|
@@ -65,7 +65,7 @@ try:
|
|
65
65
|
except ImportError:
|
66
66
|
print(
|
67
67
|
"[crypto_utils] Falling back to pure-Python AES_CBC.\n"
|
68
|
-
"Tip: pip install pycryptodome for ~800x faster speed."
|
68
|
+
"Tip: `pip install pycryptodome` for ~800x faster speed."
|
69
69
|
)
|
70
70
|
from novel_downloader.utils.crypto_utils.aes_v2 import AES_CBC
|
71
71
|
|
@@ -3,12 +3,7 @@
|
|
3
3
|
novel_downloader.utils.epub.constants
|
4
4
|
-------------------------------------
|
5
5
|
|
6
|
-
EPUB-specific constants used by the builder
|
7
|
-
* Directory names for OEBPS structure
|
8
|
-
* XML namespace URIs
|
9
|
-
* Package attributes and document-type declarations
|
10
|
-
* Media type mappings for images
|
11
|
-
* Template strings for container.xml and cover image HTML
|
6
|
+
EPUB-specific constants used by the builder.
|
12
7
|
"""
|
13
8
|
|
14
9
|
ROOT_PATH = "OEBPS"
|
@@ -34,6 +34,7 @@ class FontOCR:
|
|
34
34
|
device: str | None = None,
|
35
35
|
precision: str = "fp32",
|
36
36
|
cpu_threads: int = 10,
|
37
|
+
enable_hpi: bool = False,
|
37
38
|
**kwargs: Any,
|
38
39
|
) -> None:
|
39
40
|
"""
|
@@ -54,6 +55,7 @@ class FontOCR:
|
|
54
55
|
device=device,
|
55
56
|
precision=precision,
|
56
57
|
cpu_threads=cpu_threads,
|
58
|
+
enable_hpi=enable_hpi,
|
57
59
|
)
|
58
60
|
|
59
61
|
def predict(
|
@@ -9,6 +9,8 @@ Lazily load the FontOCR class.
|
|
9
9
|
import logging
|
10
10
|
from typing import TYPE_CHECKING
|
11
11
|
|
12
|
+
from novel_downloader.models import FontOCRConfig
|
13
|
+
|
12
14
|
if TYPE_CHECKING:
|
13
15
|
from .core import FontOCR
|
14
16
|
|
@@ -17,11 +19,7 @@ logger = logging.getLogger(__name__)
|
|
17
19
|
_FONT_OCR: "FontOCR | None" = None
|
18
20
|
|
19
21
|
|
20
|
-
def get_font_ocr(
|
21
|
-
model_name: str | None = None,
|
22
|
-
model_dir: str | None = None,
|
23
|
-
input_shape: tuple[int, int, int] | None = None,
|
24
|
-
) -> "FontOCR | None":
|
22
|
+
def get_font_ocr(cfg: FontOCRConfig) -> "FontOCR | None":
|
25
23
|
"""
|
26
24
|
Try to initialize and return a singleton FontOCR instance.
|
27
25
|
Returns None if FontOCR or its dependencies are not available.
|
@@ -32,9 +30,13 @@ def get_font_ocr(
|
|
32
30
|
from .core import FontOCR
|
33
31
|
|
34
32
|
_FONT_OCR = FontOCR(
|
35
|
-
model_name=model_name,
|
36
|
-
model_dir=model_dir,
|
37
|
-
input_shape=input_shape,
|
33
|
+
model_name=cfg.model_name,
|
34
|
+
model_dir=cfg.model_dir,
|
35
|
+
input_shape=cfg.input_shape,
|
36
|
+
device=cfg.device,
|
37
|
+
precision=cfg.precision,
|
38
|
+
cpu_threads=cfg.cpu_threads,
|
39
|
+
enable_hpi=cfg.enable_hpi,
|
38
40
|
)
|
39
41
|
except ImportError:
|
40
42
|
logger.warning(
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.node_decryptor
|
4
|
+
-------------------------------------
|
5
|
+
|
6
|
+
Provides NodeDecryptor, which ensures a Node.js environment,
|
7
|
+
downloads or installs the required JS modules (Fock + decrypt script),
|
8
|
+
and invokes a Node.js subprocess to decrypt chapter content.
|
9
|
+
"""
|
10
|
+
|
11
|
+
__all__ = ["get_decryptor"]
|
12
|
+
|
13
|
+
from .decryptor import get_decryptor
|