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.
Files changed (104) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/download.py +11 -8
  3. novel_downloader/cli/export.py +17 -17
  4. novel_downloader/cli/ui.py +28 -1
  5. novel_downloader/config/adapter.py +27 -1
  6. novel_downloader/core/archived/deqixs/fetcher.py +1 -28
  7. novel_downloader/core/downloaders/__init__.py +2 -0
  8. novel_downloader/core/downloaders/base.py +34 -85
  9. novel_downloader/core/downloaders/common.py +147 -171
  10. novel_downloader/core/downloaders/qianbi.py +30 -64
  11. novel_downloader/core/downloaders/qidian.py +157 -184
  12. novel_downloader/core/downloaders/qqbook.py +292 -0
  13. novel_downloader/core/downloaders/registry.py +2 -2
  14. novel_downloader/core/exporters/__init__.py +2 -0
  15. novel_downloader/core/exporters/base.py +37 -59
  16. novel_downloader/core/exporters/common.py +620 -0
  17. novel_downloader/core/exporters/linovelib.py +47 -0
  18. novel_downloader/core/exporters/qidian.py +41 -12
  19. novel_downloader/core/exporters/qqbook.py +28 -0
  20. novel_downloader/core/exporters/registry.py +2 -2
  21. novel_downloader/core/fetchers/__init__.py +4 -2
  22. novel_downloader/core/fetchers/aaatxt.py +2 -22
  23. novel_downloader/core/fetchers/b520.py +3 -23
  24. novel_downloader/core/fetchers/base.py +80 -105
  25. novel_downloader/core/fetchers/biquyuedu.py +2 -22
  26. novel_downloader/core/fetchers/dxmwx.py +10 -22
  27. novel_downloader/core/fetchers/esjzone.py +6 -29
  28. novel_downloader/core/fetchers/guidaye.py +2 -22
  29. novel_downloader/core/fetchers/hetushu.py +9 -29
  30. novel_downloader/core/fetchers/i25zw.py +2 -16
  31. novel_downloader/core/fetchers/ixdzs8.py +2 -16
  32. novel_downloader/core/fetchers/jpxs123.py +2 -16
  33. novel_downloader/core/fetchers/lewenn.py +2 -22
  34. novel_downloader/core/fetchers/linovelib.py +4 -20
  35. novel_downloader/core/fetchers/{eightnovel.py → n8novel.py} +12 -40
  36. novel_downloader/core/fetchers/piaotia.py +2 -16
  37. novel_downloader/core/fetchers/qbtr.py +2 -16
  38. novel_downloader/core/fetchers/qianbi.py +1 -20
  39. novel_downloader/core/fetchers/qidian.py +7 -33
  40. novel_downloader/core/fetchers/qqbook.py +177 -0
  41. novel_downloader/core/fetchers/quanben5.py +9 -29
  42. novel_downloader/core/fetchers/rate_limiter.py +22 -53
  43. novel_downloader/core/fetchers/sfacg.py +3 -16
  44. novel_downloader/core/fetchers/shencou.py +2 -16
  45. novel_downloader/core/fetchers/shuhaige.py +2 -22
  46. novel_downloader/core/fetchers/tongrenquan.py +2 -22
  47. novel_downloader/core/fetchers/ttkan.py +3 -14
  48. novel_downloader/core/fetchers/wanbengo.py +2 -22
  49. novel_downloader/core/fetchers/xiaoshuowu.py +2 -16
  50. novel_downloader/core/fetchers/xiguashuwu.py +4 -20
  51. novel_downloader/core/fetchers/xs63b.py +3 -15
  52. novel_downloader/core/fetchers/xshbook.py +2 -22
  53. novel_downloader/core/fetchers/yamibo.py +4 -28
  54. novel_downloader/core/fetchers/yibige.py +13 -26
  55. novel_downloader/core/interfaces/exporter.py +19 -7
  56. novel_downloader/core/interfaces/fetcher.py +21 -47
  57. novel_downloader/core/parsers/__init__.py +4 -2
  58. novel_downloader/core/parsers/b520.py +2 -2
  59. novel_downloader/core/parsers/base.py +4 -39
  60. novel_downloader/core/parsers/{eightnovel.py → n8novel.py} +5 -5
  61. novel_downloader/core/parsers/{qidian/main_parser.py → qidian.py} +147 -266
  62. novel_downloader/core/parsers/qqbook.py +709 -0
  63. novel_downloader/core/parsers/xiguashuwu.py +3 -4
  64. novel_downloader/core/searchers/__init__.py +2 -2
  65. novel_downloader/core/searchers/b520.py +1 -1
  66. novel_downloader/core/searchers/base.py +2 -2
  67. novel_downloader/core/searchers/{eightnovel.py → n8novel.py} +5 -5
  68. novel_downloader/models/__init__.py +2 -0
  69. novel_downloader/models/book.py +1 -0
  70. novel_downloader/models/config.py +12 -0
  71. novel_downloader/resources/config/settings.toml +23 -5
  72. novel_downloader/resources/js_scripts/expr_to_json.js +14 -0
  73. novel_downloader/resources/js_scripts/qidian_decrypt_node.js +21 -16
  74. novel_downloader/resources/js_scripts/qq_decrypt_node.js +92 -0
  75. novel_downloader/utils/constants.py +6 -0
  76. novel_downloader/utils/crypto_utils/aes_util.py +1 -1
  77. novel_downloader/utils/epub/constants.py +1 -6
  78. novel_downloader/utils/fontocr/core.py +2 -0
  79. novel_downloader/utils/fontocr/loader.py +10 -8
  80. novel_downloader/utils/node_decryptor/__init__.py +13 -0
  81. novel_downloader/utils/node_decryptor/decryptor.py +342 -0
  82. novel_downloader/{core/parsers/qidian/utils → utils/node_decryptor}/decryptor_fetcher.py +5 -6
  83. novel_downloader/web/pages/download.py +1 -1
  84. novel_downloader/web/pages/search.py +1 -1
  85. novel_downloader/web/services/task_manager.py +2 -0
  86. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/METADATA +4 -1
  87. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/RECORD +91 -94
  88. novel_downloader/core/exporters/common/__init__.py +0 -11
  89. novel_downloader/core/exporters/common/epub.py +0 -198
  90. novel_downloader/core/exporters/common/main_exporter.py +0 -64
  91. novel_downloader/core/exporters/common/txt.py +0 -146
  92. novel_downloader/core/exporters/epub_util.py +0 -215
  93. novel_downloader/core/exporters/linovelib/__init__.py +0 -11
  94. novel_downloader/core/exporters/linovelib/epub.py +0 -349
  95. novel_downloader/core/exporters/linovelib/main_exporter.py +0 -66
  96. novel_downloader/core/exporters/linovelib/txt.py +0 -139
  97. novel_downloader/core/exporters/txt_util.py +0 -67
  98. novel_downloader/core/parsers/qidian/__init__.py +0 -10
  99. novel_downloader/core/parsers/qidian/utils/__init__.py +0 -11
  100. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +0 -175
  101. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/WHEEL +0 -0
  102. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/entry_points.txt +0 -0
  103. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/licenses/LICENSE +0 -0
  104. {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
- @classmethod
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 >= cls._CONF_THRESHOLD else None
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
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
 
19
19
  @register_searcher(
20
- site_keys=["biquge", "bqg", "b520"],
20
+ site_keys=["biquge", "b520"],
21
21
  )
22
22
  class BiqugeSearcher(BaseSearcher):
23
23
  site_name = "biquge"
@@ -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, SearcherProtocol):
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.eightnovel
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=["eightnovel", "8novel"],
20
+ site_keys=["n8novel", "8novel"],
21
21
  )
22
- class EightnovelSearcher(BaseSearcher):
23
- site_name = "8novel"
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
  )
@@ -19,6 +19,7 @@ class ChapterInfoDict(TypedDict):
19
19
  title: str
20
20
  url: str
21
21
  chapterId: str
22
+ accessible: NotRequired[bool]
22
23
 
23
24
 
24
25
  class VolumeInfoDict(TypedDict):
@@ -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 = 1.0 # 最大请求速率 (requests per second), 如不设置则请设置一个较大的数或负数
8
- request_interval = 2.0 # 同一本书各章节请求间隔 (秒)
9
- raw_data_dir = "./raw_data" # 原始章节 JSON/DB 存放目录
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
- 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}
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 script.js <input.json> <output.txt>"
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, including:
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