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.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/__init__.py +2 -4
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +27 -104
- novel_downloader/cli/download.py +78 -66
- novel_downloader/cli/export.py +20 -21
- novel_downloader/cli/main.py +3 -1
- novel_downloader/cli/search.py +120 -0
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +10 -14
- novel_downloader/config/adapter.py +195 -99
- novel_downloader/config/{loader.py → file_io.py} +53 -27
- novel_downloader/core/__init__.py +14 -13
- novel_downloader/core/archived/deqixs/fetcher.py +115 -0
- novel_downloader/core/archived/deqixs/parser.py +132 -0
- novel_downloader/core/archived/deqixs/searcher.py +89 -0
- novel_downloader/core/archived/qidian/searcher.py +79 -0
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +8 -30
- novel_downloader/core/downloaders/base.py +182 -30
- novel_downloader/core/downloaders/common.py +217 -384
- novel_downloader/core/downloaders/qianbi.py +332 -4
- novel_downloader/core/downloaders/qidian.py +250 -290
- novel_downloader/core/downloaders/registry.py +69 -0
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +8 -26
- novel_downloader/core/exporters/base.py +107 -31
- novel_downloader/core/exporters/common/__init__.py +3 -4
- novel_downloader/core/exporters/common/epub.py +92 -171
- novel_downloader/core/exporters/common/main_exporter.py +14 -67
- novel_downloader/core/exporters/common/txt.py +90 -86
- novel_downloader/core/exporters/epub_util.py +184 -1327
- novel_downloader/core/exporters/linovelib/__init__.py +3 -2
- novel_downloader/core/exporters/linovelib/epub.py +165 -222
- novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
- novel_downloader/core/exporters/linovelib/txt.py +76 -66
- novel_downloader/core/exporters/qidian.py +15 -11
- novel_downloader/core/exporters/registry.py +55 -0
- novel_downloader/core/exporters/txt_util.py +67 -0
- novel_downloader/core/fetchers/__init__.py +57 -56
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
- novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
- novel_downloader/core/fetchers/biquyuedu.py +83 -0
- novel_downloader/core/fetchers/dxmwx.py +110 -0
- novel_downloader/core/fetchers/eightnovel.py +139 -0
- novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +23 -11
- novel_downloader/core/fetchers/guidaye.py +85 -0
- novel_downloader/core/fetchers/hetushu.py +92 -0
- novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +22 -26
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
- novel_downloader/core/fetchers/piaotia.py +105 -0
- novel_downloader/core/fetchers/qbtr.py +101 -0
- novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +9 -9
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
- novel_downloader/core/fetchers/quanben5.py +92 -0
- novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
- novel_downloader/core/fetchers/registry.py +60 -0
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
- novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
- novel_downloader/core/fetchers/xiguashuwu.py +177 -0
- novel_downloader/core/fetchers/xs63b.py +171 -0
- novel_downloader/core/fetchers/xshbook.py +85 -0
- novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +23 -11
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +8 -14
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +4 -17
- novel_downloader/core/interfaces/parser.py +5 -6
- novel_downloader/core/interfaces/searcher.py +26 -0
- novel_downloader/core/parsers/__init__.py +58 -22
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +63 -12
- novel_downloader/core/parsers/biquyuedu.py +133 -0
- novel_downloader/core/parsers/dxmwx.py +162 -0
- novel_downloader/core/parsers/eightnovel.py +224 -0
- novel_downloader/core/parsers/{esjzone/main_parser.py → esjzone.py} +67 -67
- novel_downloader/core/parsers/guidaye.py +128 -0
- novel_downloader/core/parsers/hetushu.py +139 -0
- novel_downloader/core/parsers/i25zw.py +137 -0
- novel_downloader/core/parsers/ixdzs8.py +186 -0
- novel_downloader/core/parsers/jpxs123.py +137 -0
- novel_downloader/core/parsers/lewenn.py +142 -0
- novel_downloader/core/parsers/{linovelib/main_parser.py → linovelib.py} +54 -65
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
- novel_downloader/core/parsers/qidian/__init__.py +2 -2
- novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
- novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
- novel_downloader/core/parsers/qidian/main_parser.py +19 -57
- novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
- novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +57 -0
- novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
- novel_downloader/core/parsers/shencou.py +215 -0
- novel_downloader/core/parsers/shuhaige.py +111 -0
- novel_downloader/core/parsers/tongrenquan.py +116 -0
- novel_downloader/core/parsers/ttkan.py +132 -0
- novel_downloader/core/parsers/wanbengo.py +191 -0
- novel_downloader/core/parsers/xiaoshuowu.py +173 -0
- novel_downloader/core/parsers/xiguashuwu.py +435 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +155 -0
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +51 -0
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/b520.py +84 -0
- novel_downloader/core/searchers/base.py +168 -0
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +102 -0
- novel_downloader/core/searchers/hetushu.py +92 -0
- novel_downloader/core/searchers/i25zw.py +93 -0
- novel_downloader/core/searchers/ixdzs8.py +107 -0
- novel_downloader/core/searchers/jpxs123.py +107 -0
- novel_downloader/core/searchers/piaotia.py +100 -0
- novel_downloader/core/searchers/qbtr.py +106 -0
- novel_downloader/core/searchers/qianbi.py +165 -0
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +79 -0
- novel_downloader/core/searchers/shuhaige.py +124 -0
- novel_downloader/core/searchers/tongrenquan.py +110 -0
- novel_downloader/core/searchers/ttkan.py +92 -0
- novel_downloader/core/searchers/xiaoshuowu.py +122 -0
- novel_downloader/core/searchers/xiguashuwu.py +95 -0
- novel_downloader/core/searchers/xs63b.py +104 -0
- novel_downloader/locales/en.json +36 -79
- novel_downloader/locales/zh.json +37 -80
- novel_downloader/models/__init__.py +23 -50
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +16 -43
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +21 -0
- novel_downloader/resources/config/settings.toml +39 -74
- novel_downloader/resources/css_styles/intro.css +83 -0
- novel_downloader/resources/css_styles/main.css +30 -89
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +43 -0
- novel_downloader/utils/chapter_storage.py +247 -226
- novel_downloader/utils/constants.py +5 -50
- novel_downloader/utils/cookies.py +6 -18
- novel_downloader/utils/crypto_utils/__init__.py +13 -0
- novel_downloader/utils/crypto_utils/aes_util.py +90 -0
- novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
- novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
- novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
- novel_downloader/utils/epub/__init__.py +34 -0
- novel_downloader/utils/epub/builder.py +377 -0
- novel_downloader/utils/epub/constants.py +118 -0
- novel_downloader/utils/epub/documents.py +297 -0
- novel_downloader/utils/epub/models.py +120 -0
- novel_downloader/utils/epub/utils.py +179 -0
- novel_downloader/utils/file_utils/__init__.py +5 -30
- novel_downloader/utils/file_utils/io.py +9 -150
- novel_downloader/utils/file_utils/normalize.py +2 -2
- novel_downloader/utils/file_utils/sanitize.py +2 -7
- novel_downloader/utils/fontocr.py +207 -0
- novel_downloader/utils/i18n.py +2 -0
- novel_downloader/utils/logger.py +10 -16
- novel_downloader/utils/network.py +111 -252
- novel_downloader/utils/state.py +5 -90
- novel_downloader/utils/text_utils/__init__.py +16 -21
- novel_downloader/utils/text_utils/diff_display.py +6 -9
- novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
- novel_downloader/utils/text_utils/text_cleaner.py +179 -0
- novel_downloader/utils/text_utils/truncate_utils.py +62 -0
- novel_downloader/utils/time_utils/__init__.py +6 -12
- novel_downloader/utils/time_utils/datetime_utils.py +23 -33
- novel_downloader/utils/time_utils/sleep_utils.py +5 -10
- novel_downloader/web/__init__.py +13 -0
- novel_downloader/web/components/__init__.py +11 -0
- novel_downloader/web/components/navigation.py +35 -0
- novel_downloader/web/main.py +66 -0
- novel_downloader/web/pages/__init__.py +17 -0
- novel_downloader/web/pages/download.py +78 -0
- novel_downloader/web/pages/progress.py +147 -0
- novel_downloader/web/pages/search.py +329 -0
- novel_downloader/web/services/__init__.py +17 -0
- novel_downloader/web/services/client_dialog.py +164 -0
- novel_downloader/web/services/cred_broker.py +113 -0
- novel_downloader/web/services/cred_models.py +35 -0
- novel_downloader/web/services/task_manager.py +264 -0
- novel_downloader-2.0.0.dist-info/METADATA +171 -0
- novel_downloader-2.0.0.dist-info/RECORD +210 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
- novel_downloader/config/site_rules.py +0 -94
- novel_downloader/core/downloaders/biquge.py +0 -25
- novel_downloader/core/downloaders/esjzone.py +0 -25
- novel_downloader/core/downloaders/linovelib.py +0 -25
- novel_downloader/core/downloaders/sfacg.py +0 -25
- novel_downloader/core/downloaders/yamibo.py +0 -25
- novel_downloader/core/exporters/biquge.py +0 -25
- novel_downloader/core/exporters/esjzone.py +0 -25
- novel_downloader/core/exporters/qianbi.py +0 -25
- novel_downloader/core/exporters/sfacg.py +0 -25
- novel_downloader/core/exporters/yamibo.py +0 -25
- novel_downloader/core/factory/__init__.py +0 -20
- novel_downloader/core/factory/downloader.py +0 -73
- novel_downloader/core/factory/exporter.py +0 -58
- novel_downloader/core/factory/fetcher.py +0 -96
- novel_downloader/core/factory/parser.py +0 -86
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -403
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/common/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -204
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -193
- novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/browser.py +0 -318
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -189
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -229
- novel_downloader/core/parsers/biquge/__init__.py +0 -10
- novel_downloader/core/parsers/biquge/main_parser.py +0 -134
- novel_downloader/core/parsers/common/__init__.py +0 -13
- novel_downloader/core/parsers/common/helper.py +0 -323
- novel_downloader/core/parsers/common/main_parser.py +0 -106
- novel_downloader/core/parsers/esjzone/__init__.py +0 -10
- novel_downloader/core/parsers/linovelib/__init__.py +0 -10
- novel_downloader/core/parsers/qianbi/__init__.py +0 -10
- novel_downloader/core/parsers/sfacg/__init__.py +0 -10
- novel_downloader/core/parsers/yamibo/__init__.py +0 -10
- novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
- novel_downloader/models/browser.py +0 -21
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/site_rules.py +0 -99
- novel_downloader/models/tasks.py +0 -33
- novel_downloader/models/types.py +0 -15
- novel_downloader/resources/css_styles/volume-intro.css +0 -56
- novel_downloader/resources/json/replace_word_map.json +0 -4
- novel_downloader/resources/text/blacklist.txt +0 -22
- novel_downloader/tui/__init__.py +0 -7
- novel_downloader/tui/app.py +0 -32
- novel_downloader/tui/main.py +0 -17
- novel_downloader/tui/screens/__init__.py +0 -14
- novel_downloader/tui/screens/home.py +0 -198
- novel_downloader/tui/screens/login.py +0 -74
- novel_downloader/tui/styles/home_layout.tcss +0 -79
- novel_downloader/tui/widgets/richlog_handler.py +0 -24
- novel_downloader/utils/cache.py +0 -24
- novel_downloader/utils/fontocr/__init__.py +0 -22
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -303
- novel_downloader/utils/fontocr/ocr_v2.py +0 -752
- novel_downloader/utils/hash_store.py +0 -279
- novel_downloader/utils/hash_utils.py +0 -103
- novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
- novel_downloader/utils/text_utils/font_mapping.py +0 -28
- novel_downloader/utils/text_utils/text_cleaning.py +0 -107
- novel_downloader-1.4.5.dist-info/METADATA +0 -196
- novel_downloader-1.4.5.dist-info/RECORD +0 -165
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
38
|
+
def write_file(
|
43
39
|
content: str | bytes | dict[Any, Any] | list[Any] | Any,
|
44
40
|
filepath: str | Path,
|
45
|
-
|
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
|
-
) ->
|
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:
|
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
|
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
|
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
|
novel_downloader/utils/i18n.py
CHANGED
novel_downloader/utils/logger.py
CHANGED
@@ -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[
|
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:
|
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
|
-
|
37
|
-
|
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:
|
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():
|