novel-downloader 1.4.5__py3-none-any.whl → 2.0.0__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 (276) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +2 -4
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +27 -104
  5. novel_downloader/cli/download.py +78 -66
  6. novel_downloader/cli/export.py +20 -21
  7. novel_downloader/cli/main.py +3 -1
  8. novel_downloader/cli/search.py +120 -0
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +10 -14
  11. novel_downloader/config/adapter.py +195 -99
  12. novel_downloader/config/{loader.py → file_io.py} +53 -27
  13. novel_downloader/core/__init__.py +14 -13
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/archived/qidian/searcher.py +79 -0
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +8 -30
  21. novel_downloader/core/downloaders/base.py +182 -30
  22. novel_downloader/core/downloaders/common.py +217 -384
  23. novel_downloader/core/downloaders/qianbi.py +332 -4
  24. novel_downloader/core/downloaders/qidian.py +250 -290
  25. novel_downloader/core/downloaders/registry.py +69 -0
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +8 -26
  28. novel_downloader/core/exporters/base.py +107 -31
  29. novel_downloader/core/exporters/common/__init__.py +3 -4
  30. novel_downloader/core/exporters/common/epub.py +92 -171
  31. novel_downloader/core/exporters/common/main_exporter.py +14 -67
  32. novel_downloader/core/exporters/common/txt.py +90 -86
  33. novel_downloader/core/exporters/epub_util.py +184 -1327
  34. novel_downloader/core/exporters/linovelib/__init__.py +3 -2
  35. novel_downloader/core/exporters/linovelib/epub.py +165 -222
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
  37. novel_downloader/core/exporters/linovelib/txt.py +76 -66
  38. novel_downloader/core/exporters/qidian.py +15 -11
  39. novel_downloader/core/exporters/registry.py +55 -0
  40. novel_downloader/core/exporters/txt_util.py +67 -0
  41. novel_downloader/core/fetchers/__init__.py +57 -56
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
  45. novel_downloader/core/fetchers/biquyuedu.py +83 -0
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +23 -11
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +22 -26
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +9 -9
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +60 -0
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +23 -11
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +8 -14
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +4 -17
  79. novel_downloader/core/interfaces/parser.py +5 -6
  80. novel_downloader/core/interfaces/searcher.py +26 -0
  81. novel_downloader/core/parsers/__init__.py +58 -22
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +63 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/{esjzone/main_parser.py → esjzone.py} +67 -67
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/{linovelib/main_parser.py → linovelib.py} +54 -65
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
  99. novel_downloader/core/parsers/qidian/__init__.py +2 -2
  100. novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
  101. novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
  102. novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
  103. novel_downloader/core/parsers/qidian/main_parser.py +19 -57
  104. novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
  105. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
  106. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  107. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  108. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
  109. novel_downloader/core/parsers/quanben5.py +103 -0
  110. novel_downloader/core/parsers/registry.py +57 -0
  111. novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
  112. novel_downloader/core/parsers/shencou.py +215 -0
  113. novel_downloader/core/parsers/shuhaige.py +111 -0
  114. novel_downloader/core/parsers/tongrenquan.py +116 -0
  115. novel_downloader/core/parsers/ttkan.py +132 -0
  116. novel_downloader/core/parsers/wanbengo.py +191 -0
  117. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  118. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  119. novel_downloader/core/parsers/xs63b.py +161 -0
  120. novel_downloader/core/parsers/xshbook.py +134 -0
  121. novel_downloader/core/parsers/yamibo.py +155 -0
  122. novel_downloader/core/parsers/yibige.py +166 -0
  123. novel_downloader/core/searchers/__init__.py +51 -0
  124. novel_downloader/core/searchers/aaatxt.py +107 -0
  125. novel_downloader/core/searchers/b520.py +84 -0
  126. novel_downloader/core/searchers/base.py +168 -0
  127. novel_downloader/core/searchers/dxmwx.py +105 -0
  128. novel_downloader/core/searchers/eightnovel.py +84 -0
  129. novel_downloader/core/searchers/esjzone.py +102 -0
  130. novel_downloader/core/searchers/hetushu.py +92 -0
  131. novel_downloader/core/searchers/i25zw.py +93 -0
  132. novel_downloader/core/searchers/ixdzs8.py +107 -0
  133. novel_downloader/core/searchers/jpxs123.py +107 -0
  134. novel_downloader/core/searchers/piaotia.py +100 -0
  135. novel_downloader/core/searchers/qbtr.py +106 -0
  136. novel_downloader/core/searchers/qianbi.py +165 -0
  137. novel_downloader/core/searchers/quanben5.py +144 -0
  138. novel_downloader/core/searchers/registry.py +79 -0
  139. novel_downloader/core/searchers/shuhaige.py +124 -0
  140. novel_downloader/core/searchers/tongrenquan.py +110 -0
  141. novel_downloader/core/searchers/ttkan.py +92 -0
  142. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  143. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  144. novel_downloader/core/searchers/xs63b.py +104 -0
  145. novel_downloader/locales/en.json +36 -79
  146. novel_downloader/locales/zh.json +37 -80
  147. novel_downloader/models/__init__.py +23 -50
  148. novel_downloader/models/book.py +44 -0
  149. novel_downloader/models/config.py +16 -43
  150. novel_downloader/models/login.py +1 -1
  151. novel_downloader/models/search.py +21 -0
  152. novel_downloader/resources/config/settings.toml +39 -74
  153. novel_downloader/resources/css_styles/intro.css +83 -0
  154. novel_downloader/resources/css_styles/main.css +30 -89
  155. novel_downloader/resources/json/xiguashuwu.json +718 -0
  156. novel_downloader/utils/__init__.py +43 -0
  157. novel_downloader/utils/chapter_storage.py +247 -226
  158. novel_downloader/utils/constants.py +5 -50
  159. novel_downloader/utils/cookies.py +6 -18
  160. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  161. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  162. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  163. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  164. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  165. novel_downloader/utils/epub/__init__.py +34 -0
  166. novel_downloader/utils/epub/builder.py +377 -0
  167. novel_downloader/utils/epub/constants.py +118 -0
  168. novel_downloader/utils/epub/documents.py +297 -0
  169. novel_downloader/utils/epub/models.py +120 -0
  170. novel_downloader/utils/epub/utils.py +179 -0
  171. novel_downloader/utils/file_utils/__init__.py +5 -30
  172. novel_downloader/utils/file_utils/io.py +9 -150
  173. novel_downloader/utils/file_utils/normalize.py +2 -2
  174. novel_downloader/utils/file_utils/sanitize.py +2 -7
  175. novel_downloader/utils/fontocr.py +207 -0
  176. novel_downloader/utils/i18n.py +2 -0
  177. novel_downloader/utils/logger.py +10 -16
  178. novel_downloader/utils/network.py +111 -252
  179. novel_downloader/utils/state.py +5 -90
  180. novel_downloader/utils/text_utils/__init__.py +16 -21
  181. novel_downloader/utils/text_utils/diff_display.py +6 -9
  182. novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
  183. novel_downloader/utils/text_utils/text_cleaner.py +179 -0
  184. novel_downloader/utils/text_utils/truncate_utils.py +62 -0
  185. novel_downloader/utils/time_utils/__init__.py +6 -12
  186. novel_downloader/utils/time_utils/datetime_utils.py +23 -33
  187. novel_downloader/utils/time_utils/sleep_utils.py +5 -10
  188. novel_downloader/web/__init__.py +13 -0
  189. novel_downloader/web/components/__init__.py +11 -0
  190. novel_downloader/web/components/navigation.py +35 -0
  191. novel_downloader/web/main.py +66 -0
  192. novel_downloader/web/pages/__init__.py +17 -0
  193. novel_downloader/web/pages/download.py +78 -0
  194. novel_downloader/web/pages/progress.py +147 -0
  195. novel_downloader/web/pages/search.py +329 -0
  196. novel_downloader/web/services/__init__.py +17 -0
  197. novel_downloader/web/services/client_dialog.py +164 -0
  198. novel_downloader/web/services/cred_broker.py +113 -0
  199. novel_downloader/web/services/cred_models.py +35 -0
  200. novel_downloader/web/services/task_manager.py +264 -0
  201. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  202. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  203. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  204. novel_downloader/config/site_rules.py +0 -94
  205. novel_downloader/core/downloaders/biquge.py +0 -25
  206. novel_downloader/core/downloaders/esjzone.py +0 -25
  207. novel_downloader/core/downloaders/linovelib.py +0 -25
  208. novel_downloader/core/downloaders/sfacg.py +0 -25
  209. novel_downloader/core/downloaders/yamibo.py +0 -25
  210. novel_downloader/core/exporters/biquge.py +0 -25
  211. novel_downloader/core/exporters/esjzone.py +0 -25
  212. novel_downloader/core/exporters/qianbi.py +0 -25
  213. novel_downloader/core/exporters/sfacg.py +0 -25
  214. novel_downloader/core/exporters/yamibo.py +0 -25
  215. novel_downloader/core/factory/__init__.py +0 -20
  216. novel_downloader/core/factory/downloader.py +0 -73
  217. novel_downloader/core/factory/exporter.py +0 -58
  218. novel_downloader/core/factory/fetcher.py +0 -96
  219. novel_downloader/core/factory/parser.py +0 -86
  220. novel_downloader/core/fetchers/base/__init__.py +0 -14
  221. novel_downloader/core/fetchers/base/browser.py +0 -403
  222. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  223. novel_downloader/core/fetchers/common/__init__.py +0 -14
  224. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  225. novel_downloader/core/fetchers/esjzone/browser.py +0 -204
  226. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  227. novel_downloader/core/fetchers/linovelib/browser.py +0 -193
  228. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  229. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  230. novel_downloader/core/fetchers/qidian/browser.py +0 -318
  231. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  232. novel_downloader/core/fetchers/sfacg/browser.py +0 -189
  233. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  234. novel_downloader/core/fetchers/yamibo/browser.py +0 -229
  235. novel_downloader/core/parsers/biquge/__init__.py +0 -10
  236. novel_downloader/core/parsers/biquge/main_parser.py +0 -134
  237. novel_downloader/core/parsers/common/__init__.py +0 -13
  238. novel_downloader/core/parsers/common/helper.py +0 -323
  239. novel_downloader/core/parsers/common/main_parser.py +0 -106
  240. novel_downloader/core/parsers/esjzone/__init__.py +0 -10
  241. novel_downloader/core/parsers/linovelib/__init__.py +0 -10
  242. novel_downloader/core/parsers/qianbi/__init__.py +0 -10
  243. novel_downloader/core/parsers/sfacg/__init__.py +0 -10
  244. novel_downloader/core/parsers/yamibo/__init__.py +0 -10
  245. novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
  246. novel_downloader/models/browser.py +0 -21
  247. novel_downloader/models/chapter.py +0 -25
  248. novel_downloader/models/site_rules.py +0 -99
  249. novel_downloader/models/tasks.py +0 -33
  250. novel_downloader/models/types.py +0 -15
  251. novel_downloader/resources/css_styles/volume-intro.css +0 -56
  252. novel_downloader/resources/json/replace_word_map.json +0 -4
  253. novel_downloader/resources/text/blacklist.txt +0 -22
  254. novel_downloader/tui/__init__.py +0 -7
  255. novel_downloader/tui/app.py +0 -32
  256. novel_downloader/tui/main.py +0 -17
  257. novel_downloader/tui/screens/__init__.py +0 -14
  258. novel_downloader/tui/screens/home.py +0 -198
  259. novel_downloader/tui/screens/login.py +0 -74
  260. novel_downloader/tui/styles/home_layout.tcss +0 -79
  261. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  262. novel_downloader/utils/cache.py +0 -24
  263. novel_downloader/utils/fontocr/__init__.py +0 -22
  264. novel_downloader/utils/fontocr/model_loader.py +0 -69
  265. novel_downloader/utils/fontocr/ocr_v1.py +0 -303
  266. novel_downloader/utils/fontocr/ocr_v2.py +0 -752
  267. novel_downloader/utils/hash_store.py +0 -279
  268. novel_downloader/utils/hash_utils.py +0 -103
  269. novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
  270. novel_downloader/utils/text_utils/font_mapping.py +0 -28
  271. novel_downloader/utils/text_utils/text_cleaning.py +0 -107
  272. novel_downloader-1.4.5.dist-info/METADATA +0 -196
  273. novel_downloader-1.4.5.dist-info/RECORD +0 -165
  274. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  275. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  276. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -3,18 +3,14 @@
3
3
  novel_downloader.utils.file_utils.io
4
4
  ------------------------------------
5
5
 
6
- File I/O utilities for reading and writing text, JSON, and binary data.
7
-
8
- Includes:
9
- - Safe, atomic file saving with optional overwrite and auto-renaming
10
- - JSON pretty-printing with size-aware formatting
11
- - Simple helpers for reading files with fallback and logging
6
+ File I/O utilities for reading and writing data.
12
7
  """
13
8
 
9
+ __all__ = ["write_file"]
10
+
14
11
  import json
15
12
  import logging
16
13
  import tempfile
17
- from importlib.resources import files
18
14
  from pathlib import Path
19
15
  from typing import Any, Literal
20
16
 
@@ -39,15 +35,15 @@ def _get_non_conflicting_path(path: Path) -> Path:
39
35
  return new_path
40
36
 
41
37
 
42
- def _write_file(
38
+ def write_file(
43
39
  content: str | bytes | dict[Any, Any] | list[Any] | Any,
44
40
  filepath: str | Path,
45
- mode: str | None = None,
41
+ write_mode: str = "w",
46
42
  *,
47
43
  on_exist: Literal["overwrite", "skip", "rename"] = "overwrite",
48
44
  dump_json: bool = False,
49
45
  encoding: str = "utf-8",
50
- ) -> bool:
46
+ ) -> Path | None:
51
47
  """
52
48
  Write content to a file safely with optional atomic behavior
53
49
  and JSON serialization.
@@ -60,7 +56,7 @@ def _write_file(
60
56
  or 'rename'.
61
57
  :param dump_json: If True, serialize content as JSON.
62
58
  :param encoding: Text encoding for writing.
63
- :return: True if writing succeeds, False otherwise.
59
+ :return: Path if writing succeeds, None otherwise.
64
60
  """
65
61
  path = Path(filepath)
66
62
  path = path.with_name(sanitize_filename(path.name))
@@ -69,7 +65,7 @@ def _write_file(
69
65
  if path.exists():
70
66
  if on_exist == "skip":
71
67
  logger.debug("[file] '%s' exists, skipping", path)
72
- return False
68
+ return path
73
69
  if on_exist == "rename":
74
70
  path = _get_non_conflicting_path(path)
75
71
  logger.debug("[file] Renaming target to avoid conflict: %s", path)
@@ -104,144 +100,7 @@ def _write_file(
104
100
  tmp_path = Path(tmp.name)
105
101
  tmp_path.replace(path)
106
102
  logger.debug("[file] '%s' written successfully", path)
107
- return True
103
+ return path
108
104
  except Exception as exc:
109
105
  logger.warning("[file] Error writing %r: %s", path, exc)
110
- return False
111
-
112
-
113
- def save_as_txt(
114
- content: str,
115
- filepath: str | Path,
116
- *,
117
- encoding: str = "utf-8",
118
- on_exist: Literal["overwrite", "skip", "rename"] = "overwrite",
119
- ) -> bool:
120
- """
121
- Save plain text content to the given file path.
122
-
123
- :param content: Text content to write.
124
- :param filepath: Destination file path.
125
- :param encoding: Text encoding to use (default: 'utf-8').
126
- :param on_exist: How to handle existing files: 'overwrite', 'skip', or 'rename'.
127
- :return: True if successful, False otherwise.
128
- """
129
- return _write_file(
130
- content=content,
131
- filepath=filepath,
132
- mode="w",
133
- on_exist=on_exist,
134
- dump_json=False,
135
- encoding=encoding,
136
- )
137
-
138
-
139
- def save_as_json(
140
- content: Any,
141
- filepath: str | Path,
142
- *,
143
- encoding: str = "utf-8",
144
- on_exist: Literal["overwrite", "skip", "rename"] = "overwrite",
145
- ) -> bool:
146
- """
147
- Save JSON-serializable content to the given file path.
148
-
149
- :param content: Data to write as JSON.
150
- :param filepath: Destination file path.
151
- :param encoding: Text encoding to use (default: 'utf-8').
152
- :param on_exist: How to handle existing files: 'overwrite', 'skip', or 'rename'.
153
- :return: True if successful, False otherwise.
154
- """
155
- return _write_file(
156
- content=content,
157
- filepath=filepath,
158
- mode="w",
159
- on_exist=on_exist,
160
- dump_json=True,
161
- encoding=encoding,
162
- )
163
-
164
-
165
- def read_text_file(filepath: str | Path, encoding: str = "utf-8") -> str | None:
166
- """
167
- Read a UTF-8 text file.
168
-
169
- :param filepath: Path to file.
170
- :param encoding: Encoding to use.
171
- :return: Text content or None on failure.
172
- """
173
- path = Path(filepath)
174
- try:
175
- return path.read_text(encoding=encoding)
176
- except Exception as e:
177
- logger.warning("[file] Failed to read %r: %s", path, e)
178
106
  return None
179
-
180
-
181
- def read_json_file(filepath: str | Path, encoding: str = "utf-8") -> Any | None:
182
- """
183
- Read a JSON file and parse it into Python objects.
184
-
185
- :param filepath: Path to file.
186
- :param encoding: Encoding to use.
187
- :return: Python object or None on failure.
188
- """
189
- path = Path(filepath)
190
- try:
191
- return json.loads(path.read_text(encoding=encoding))
192
- except Exception as e:
193
- logger.warning("[file] Failed to read %r: %s", path, e)
194
- return None
195
-
196
-
197
- def read_binary_file(filepath: str | Path) -> bytes | None:
198
- """
199
- Read a binary file and return its content as bytes.
200
-
201
- :param filepath: Path to file.
202
- :return: Bytes or None on failure.
203
- """
204
- path = Path(filepath)
205
- try:
206
- return path.read_bytes()
207
- except Exception as e:
208
- logger.warning("[file] Failed to read %r: %s", path, e)
209
- return None
210
-
211
-
212
- def load_text_resource(
213
- filename: str,
214
- package: str = "novel_downloader.resources.text",
215
- ) -> str:
216
- """
217
- Load and return the contents of a text resource.
218
-
219
- :param filename: Name of the text file (e.g. "blacklist.txt").
220
- :param package: Package path where resources live (default: text resources).
221
- For other resource types, point to the appropriate subpackage
222
- (e.g. "novel_downloader.resources.css").
223
- :return: File contents as a string.
224
- """
225
- resource_path = files(package).joinpath(filename)
226
- return resource_path.read_text(encoding="utf-8")
227
-
228
-
229
- def load_blacklisted_words() -> set[str]:
230
- """
231
- Convenience loader for the blacklist.txt in the text resources.
232
-
233
- :return: A set of non-empty, stripped lines from blacklist.txt.
234
- """
235
- text = load_text_resource("blacklist.txt")
236
- return {line.strip() for line in text.splitlines() if line.strip()}
237
-
238
-
239
- __all__ = [
240
- "save_as_txt",
241
- "save_as_json",
242
- "read_text_file",
243
- "read_json_file",
244
- "read_binary_file",
245
- "load_text_resource",
246
- "load_blacklisted_words",
247
- ]
@@ -9,6 +9,8 @@ across platforms or output formats.
9
9
  Currently includes line-ending normalization for .txt files.
10
10
  """
11
11
 
12
+ __all__ = ["normalize_txt_line_endings"]
13
+
12
14
  import logging
13
15
  from pathlib import Path
14
16
 
@@ -46,8 +48,6 @@ def normalize_txt_line_endings(folder_path: str | Path) -> None:
46
48
  return
47
49
 
48
50
 
49
- __all__ = ["normalize_txt_line_endings"]
50
-
51
51
  if __name__ == "__main__": # pragma: no cover
52
52
  import argparse
53
53
 
@@ -5,12 +5,10 @@ novel_downloader.utils.file_utils.sanitize
5
5
 
6
6
  Utility functions for cleaning and validating filenames for safe use
7
7
  on different operating systems.
8
-
9
- This module provides a cross-platform `sanitize_filename` function
10
- that replaces or removes illegal characters from filenames, trims
11
- lengths, and avoids reserved names on Windows systems.
12
8
  """
13
9
 
10
+ __all__ = ["sanitize_filename"]
11
+
14
12
  import logging
15
13
  import os
16
14
  import re
@@ -65,6 +63,3 @@ def sanitize_filename(filename: str, max_length: int | None = 255) -> str:
65
63
  cleaned = "_untitled"
66
64
  logger.debug("[file] Sanitized filename: %r -> %r", filename, cleaned)
67
65
  return cleaned
68
-
69
-
70
- __all__ = ["sanitize_filename"]
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.fontocr
4
+ ------------------------------
5
+
6
+ This class provides utility methods for optical character recognition (OCR),
7
+ primarily used for decrypting custom font encryption.
8
+ """
9
+
10
+ __all__ = [
11
+ "FontOCR",
12
+ "get_font_ocr",
13
+ ]
14
+ __version__ = "4.0"
15
+
16
+ import logging
17
+ from collections.abc import Generator
18
+ from typing import Any, TypeVar
19
+
20
+ import numpy as np
21
+ from paddleocr import TextRecognition # takes 5 ~ 12 sec to init
22
+ from PIL import Image, ImageDraw, ImageFont
23
+ from PIL.Image import Transpose
24
+
25
+ T = TypeVar("T")
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class FontOCR:
30
+ """
31
+ Version 4 of the FontOCR utility.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ model_name: str | None = None,
37
+ model_dir: str | None = None,
38
+ input_shape: tuple[int, int, int] | None = None,
39
+ device: str | None = None,
40
+ precision: str = "fp32",
41
+ cpu_threads: int = 10,
42
+ batch_size: int = 32,
43
+ threshold: float = 0.0,
44
+ **kwargs: Any,
45
+ ) -> None:
46
+ """
47
+ Initialize a FontOCR instance.
48
+
49
+ :param batch_size: batch size for OCR inference (minimum 1)
50
+ :param ocr_weight: weight factor for OCR-based prediction scores
51
+ :param vec_weight: weight factor for vector-based similarity scores
52
+ :param threshold: minimum confidence threshold for predictions [0.0-1.0]
53
+ :param kwargs: reserved for future extensions
54
+ """
55
+ self._batch_size = batch_size
56
+ self._threshold = threshold
57
+ self._ocr_model = TextRecognition(
58
+ model_name=model_name,
59
+ model_dir=model_dir,
60
+ input_shape=input_shape,
61
+ device=device,
62
+ precision=precision,
63
+ cpu_threads=cpu_threads,
64
+ )
65
+
66
+ def predict(
67
+ self,
68
+ images: list[np.ndarray],
69
+ top_k: int = 1,
70
+ ) -> list[list[tuple[str, float]]]:
71
+ """
72
+ Run OCR on input images.
73
+
74
+ :param images: list of np.ndarray objects to predict
75
+ :param top_k: number of top candidates to return per image
76
+ :return: list of lists containing (character, score)
77
+ """
78
+ return [
79
+ [(pred.get("rec_text"), pred.get("rec_score"))]
80
+ for pred in self._ocr_model.predict(images, batch_size=self._batch_size)
81
+ ]
82
+
83
+ @staticmethod
84
+ def render_char_image(
85
+ char: str,
86
+ render_font: ImageFont.FreeTypeFont,
87
+ is_reflect: bool = False,
88
+ size: int = 64,
89
+ ) -> Image.Image | None:
90
+ """
91
+ Render a single character into an RGB square image.
92
+
93
+ :param char: character to render
94
+ :param render_font: FreeTypeFont instance to render with
95
+ :param is_reflect: if True, flip the image horizontally
96
+ :param size: output image size (width and height in pixels)
97
+ :return: rendered PIL.Image in RGB or None if blank
98
+ """
99
+ # img = Image.new("L", (size, size), color=255)
100
+ img = Image.new("RGB", (size, size), color=(255, 255, 255))
101
+ draw = ImageDraw.Draw(img)
102
+ bbox = draw.textbbox((0, 0), char, font=render_font)
103
+ w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
104
+ x = (size - w) // 2 - bbox[0]
105
+ y = (size - h) // 2 - bbox[1]
106
+ draw.text((x, y), char, fill=0, font=render_font)
107
+ if is_reflect:
108
+ img = img.transpose(Transpose.FLIP_LEFT_RIGHT)
109
+
110
+ img_np = np.array(img)
111
+ if np.unique(img_np).size == 1:
112
+ return None
113
+
114
+ return img
115
+
116
+ @staticmethod
117
+ def render_char_image_array(
118
+ char: str,
119
+ render_font: ImageFont.FreeTypeFont,
120
+ is_reflect: bool = False,
121
+ size: int = 64,
122
+ ) -> np.ndarray | None:
123
+ """
124
+ Render a single character into an RGB square image.
125
+
126
+ :param char: character to render
127
+ :param render_font: FreeTypeFont instance to render with
128
+ :param is_reflect: if True, flip the image horizontally
129
+ :param size: output image size (width and height in pixels)
130
+ :return: rendered image as np.ndarray in RGB or None if blank
131
+ """
132
+ # img = Image.new("L", (size, size), color=255)
133
+ img = Image.new("RGB", (size, size), color=(255, 255, 255))
134
+ draw = ImageDraw.Draw(img)
135
+ bbox = draw.textbbox((0, 0), char, font=render_font)
136
+ w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
137
+ x = (size - w) // 2 - bbox[0]
138
+ y = (size - h) // 2 - bbox[1]
139
+ draw.text((x, y), char, fill=0, font=render_font)
140
+ if is_reflect:
141
+ img = img.transpose(Transpose.FLIP_LEFT_RIGHT)
142
+
143
+ img_np = np.array(img)
144
+ if np.unique(img_np).size == 1:
145
+ return None
146
+
147
+ return img_np
148
+
149
+ @staticmethod
150
+ def render_text_image(
151
+ text: str,
152
+ font: ImageFont.FreeTypeFont,
153
+ cell_size: int = 64,
154
+ chars_per_line: int = 16,
155
+ ) -> Image.Image:
156
+ """
157
+ Render a string into a image.
158
+ """
159
+ # import textwrap
160
+ # lines = textwrap.wrap(text, width=chars_per_line) or [""]
161
+ lines = [
162
+ text[i : i + chars_per_line] for i in range(0, len(text), chars_per_line)
163
+ ] or [""]
164
+ img_w = cell_size * chars_per_line
165
+ img_h = cell_size * len(lines)
166
+
167
+ # img = Image.new("L", (img_w, img_h), color=255)
168
+ img = Image.new("RGB", (img_w, img_h), color=(255, 255, 255))
169
+ draw = ImageDraw.Draw(img)
170
+ for row, line in enumerate(lines):
171
+ for col, ch in enumerate(line):
172
+ x = (col + 0.5) * cell_size
173
+ y = (row + 0.5) * cell_size
174
+ draw.text((x, y), ch, font=font, fill=0, anchor="mm")
175
+
176
+ return img
177
+
178
+ @staticmethod
179
+ def _chunked(seq: list[T], size: int) -> Generator[list[T], None, None]:
180
+ """
181
+ Yield successive chunks of `seq` of length `size`.
182
+ """
183
+ for i in range(0, len(seq), size):
184
+ yield seq[i : i + size]
185
+
186
+
187
+ _font_ocr: FontOCR | None = None
188
+
189
+
190
+ def get_font_ocr(
191
+ model_name: str | None = None,
192
+ model_dir: str | None = None,
193
+ input_shape: tuple[int, int, int] | None = None,
194
+ batch_size: int = 32,
195
+ ) -> FontOCR:
196
+ """
197
+ Return the singleton FontOCR, initializing it on first use.
198
+ """
199
+ global _font_ocr
200
+ if _font_ocr is None:
201
+ _font_ocr = FontOCR(
202
+ model_name=model_name,
203
+ model_dir=model_dir,
204
+ input_shape=input_shape,
205
+ batch_size=batch_size,
206
+ )
207
+ return _font_ocr
@@ -6,6 +6,8 @@ novel_downloader.utils.i18n
6
6
  Multilingual text dictionary and utility for CLI and interactive mode.
7
7
  """
8
8
 
9
+ __all__ = ["t"]
10
+
9
11
  import json
10
12
  from typing import Any
11
13
 
@@ -7,16 +7,16 @@ Provides a configurable logging setup for Python applications.
7
7
  Log files are rotated daily and named with the given logger name and current date.
8
8
  """
9
9
 
10
+ __all__ = ["setup_logging"]
11
+
10
12
  import logging
11
13
  from datetime import datetime
12
14
  from logging.handlers import TimedRotatingFileHandler
13
15
  from pathlib import Path
14
16
 
15
- from novel_downloader.models import LogLevel
16
-
17
17
  from .constants import LOGGER_DIR, LOGGER_NAME
18
18
 
19
- LOG_LEVELS: dict[LogLevel, int] = {
19
+ LOG_LEVELS: dict[str, int] = {
20
20
  "DEBUG": logging.DEBUG,
21
21
  "INFO": logging.INFO,
22
22
  "WARNING": logging.WARNING,
@@ -26,19 +26,16 @@ LOG_LEVELS: dict[LogLevel, int] = {
26
26
 
27
27
  def setup_logging(
28
28
  log_filename_prefix: str | None = None,
29
- log_level: LogLevel | None = None,
29
+ log_level: str | None = None,
30
30
  log_dir: str | Path | None = None,
31
31
  ) -> logging.Logger:
32
32
  """
33
33
  Create and configure a logger for both console and rotating file output.
34
34
 
35
35
  :param log_filename_prefix: Prefix for the log file name.
36
- If None, will use the last part of `logger_name`.
37
- :param log_level: Minimum log level to show in console:
38
- "DEBUG", "INFO", "WARNING", or "ERROR".
39
- Defaults to "INFO" if not specified.
36
+ :param log_level: Minimum log level to show in console
37
+ ("DEBUG", "INFO", "WARNING", "ERROR")
40
38
  :param log_dir: Directory where log files will be saved.
41
- Defaults to "./logs" if not specified.
42
39
  :return: A fully configured logger instance.
43
40
  """
44
41
  ft_logger = logging.getLogger("fontTools.ttLib.tables._p_o_s_t")
@@ -46,12 +43,8 @@ def setup_logging(
46
43
  ft_logger.propagate = False
47
44
 
48
45
  # Determine console level (default INFO)
49
- level_str: LogLevel = log_level or "INFO"
50
- console_level = LOG_LEVELS.get(level_str)
51
- if console_level is None:
52
- raise ValueError(
53
- f"Invalid log level: {level_str}. Must be one of {list(LOG_LEVELS.keys())}"
54
- )
46
+ level_str: str = log_level or "INFO"
47
+ console_level: int = LOG_LEVELS.get(level_str) or logging.INFO
55
48
 
56
49
  # Resolve log file path
57
50
  log_path = Path(log_dir) if log_dir else LOGGER_DIR
@@ -64,8 +57,9 @@ def setup_logging(
64
57
  log_filename = log_path / f"{log_filename_prefix}_{date_str}.log"
65
58
 
66
59
  # Create or retrieve logger
67
- logger = logging.getLogger()
60
+ logger = logging.getLogger(LOGGER_NAME)
68
61
  logger.setLevel(logging.DEBUG) # Capture everything, filter by handlers
62
+ logger.propagate = False
69
63
 
70
64
  # Clear existing handlers to avoid duplicate logs
71
65
  if logger.hasHandlers():