novel-downloader 2.0.0__tar.gz → 2.0.2__tar.gz
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-2.0.0/src/novel_downloader.egg-info → novel_downloader-2.0.2}/PKG-INFO +5 -1
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/README.md +3 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/pyproject.toml +2 -1
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/__init__.py +1 -1
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/download.py +14 -11
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/export.py +19 -19
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/ui.py +35 -8
- novel_downloader-2.0.2/src/novel_downloader/config/adapter.py +383 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/__init__.py +5 -6
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/deqixs/fetcher.py +1 -28
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/__init__.py +2 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/base.py +34 -85
- novel_downloader-2.0.2/src/novel_downloader/core/downloaders/common.py +257 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/qianbi.py +30 -64
- novel_downloader-2.0.2/src/novel_downloader/core/downloaders/qidian.py +309 -0
- novel_downloader-2.0.2/src/novel_downloader/core/downloaders/qqbook.py +292 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/registry.py +2 -2
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/exporters/__init__.py +2 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/exporters/base.py +37 -59
- novel_downloader-2.0.2/src/novel_downloader/core/exporters/common.py +620 -0
- novel_downloader-2.0.2/src/novel_downloader/core/exporters/linovelib.py +47 -0
- novel_downloader-2.0.2/src/novel_downloader/core/exporters/qidian.py +61 -0
- novel_downloader-2.0.2/src/novel_downloader/core/exporters/qqbook.py +28 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/exporters/registry.py +2 -2
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/__init__.py +4 -2
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/aaatxt.py +2 -22
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/b520.py +3 -23
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/base.py +80 -105
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/biquyuedu.py +2 -22
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/dxmwx.py +10 -22
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/esjzone.py +6 -29
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/guidaye.py +2 -22
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/hetushu.py +9 -29
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/i25zw.py +2 -16
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/ixdzs8.py +2 -16
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/jpxs123.py +2 -16
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/lewenn.py +2 -22
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/linovelib.py +4 -20
- novel_downloader-2.0.0/src/novel_downloader/core/fetchers/eightnovel.py → novel_downloader-2.0.2/src/novel_downloader/core/fetchers/n8novel.py +12 -40
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/piaotia.py +2 -16
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/qbtr.py +2 -16
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/qianbi.py +1 -20
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/qidian.py +27 -68
- novel_downloader-2.0.2/src/novel_downloader/core/fetchers/qqbook.py +177 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/quanben5.py +9 -29
- novel_downloader-2.0.2/src/novel_downloader/core/fetchers/rate_limiter.py +55 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/sfacg.py +3 -16
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/shencou.py +2 -16
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/shuhaige.py +2 -22
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/tongrenquan.py +2 -22
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/ttkan.py +3 -14
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/wanbengo.py +2 -22
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/xiaoshuowu.py +2 -16
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/xiguashuwu.py +4 -20
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/xs63b.py +3 -15
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/xshbook.py +2 -22
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/yamibo.py +4 -28
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/yibige.py +13 -26
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/exporter.py +19 -7
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/fetcher.py +23 -49
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/parser.py +2 -2
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/__init__.py +4 -2
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/b520.py +2 -2
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/base.py +5 -39
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/esjzone.py +3 -3
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/eightnovel.py → novel_downloader-2.0.2/src/novel_downloader/core/parsers/n8novel.py +7 -7
- novel_downloader-2.0.2/src/novel_downloader/core/parsers/qidian.py +717 -0
- novel_downloader-2.0.2/src/novel_downloader/core/parsers/qqbook.py +709 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/xiguashuwu.py +8 -15
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/__init__.py +2 -2
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/b520.py +1 -1
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/base.py +2 -2
- novel_downloader-2.0.0/src/novel_downloader/core/searchers/eightnovel.py → novel_downloader-2.0.2/src/novel_downloader/core/searchers/n8novel.py +5 -5
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/locales/en.json +3 -3
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/locales/zh.json +3 -3
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/__init__.py +2 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/book.py +1 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/config.py +12 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/config/settings.toml +23 -5
- novel_downloader-2.0.2/src/novel_downloader/resources/js_scripts/expr_to_json.js +14 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/js_scripts/qidian_decrypt_node.js +21 -16
- novel_downloader-2.0.2/src/novel_downloader/resources/js_scripts/qq_decrypt_node.js +92 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/__init__.py +0 -2
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/chapter_storage.py +2 -3
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/constants.py +7 -3
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/cookies.py +32 -17
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/crypto_utils/__init__.py +0 -6
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/crypto_utils/aes_util.py +1 -1
- novel_downloader-2.0.2/src/novel_downloader/utils/crypto_utils/rc4.py +54 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/__init__.py +2 -3
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/builder.py +6 -6
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/constants.py +1 -6
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/documents.py +7 -7
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/models.py +8 -8
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/utils.py +10 -10
- novel_downloader-2.0.2/src/novel_downloader/utils/file_utils/io.py +81 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/file_utils/normalize.py +1 -7
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/file_utils/sanitize.py +4 -11
- novel_downloader-2.0.2/src/novel_downloader/utils/fontocr/__init__.py +13 -0
- novel_downloader-2.0.0/src/novel_downloader/utils/fontocr.py → novel_downloader-2.0.2/src/novel_downloader/utils/fontocr/core.py +72 -61
- novel_downloader-2.0.2/src/novel_downloader/utils/fontocr/loader.py +52 -0
- novel_downloader-2.0.2/src/novel_downloader/utils/logger.py +120 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/network.py +16 -40
- novel_downloader-2.0.2/src/novel_downloader/utils/node_decryptor/__init__.py +13 -0
- novel_downloader-2.0.2/src/novel_downloader/utils/node_decryptor/decryptor.py +342 -0
- {novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils → novel_downloader-2.0.2/src/novel_downloader/utils/node_decryptor}/decryptor_fetcher.py +5 -6
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/text_cleaner.py +39 -30
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/truncate_utils.py +3 -14
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/time_utils/sleep_utils.py +53 -43
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/main.py +1 -1
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/pages/download.py +1 -1
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/pages/search.py +4 -4
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/task_manager.py +2 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2/src/novel_downloader.egg-info}/PKG-INFO +5 -1
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader.egg-info/SOURCES.txt +18 -25
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader.egg-info/requires.txt +1 -0
- novel_downloader-2.0.0/src/novel_downloader/config/adapter.py +0 -320
- novel_downloader-2.0.0/src/novel_downloader/core/downloaders/common.py +0 -281
- novel_downloader-2.0.0/src/novel_downloader/core/downloaders/qidian.py +0 -336
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/common/__init__.py +0 -11
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/common/epub.py +0 -198
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/common/main_exporter.py +0 -64
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/common/txt.py +0 -146
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/epub_util.py +0 -215
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/linovelib/__init__.py +0 -11
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/linovelib/epub.py +0 -349
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/linovelib/main_exporter.py +0 -66
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/linovelib/txt.py +0 -139
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/qidian.py +0 -32
- novel_downloader-2.0.0/src/novel_downloader/core/exporters/txt_util.py +0 -67
- novel_downloader-2.0.0/src/novel_downloader/core/fetchers/rate_limiter.py +0 -86
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/__init__.py +0 -10
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/book_info_parser.py +0 -89
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -470
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/chapter_normal.py +0 -126
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/main_parser.py +0 -101
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/__init__.py +0 -30
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +0 -143
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/helpers.py +0 -110
- novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/node_decryptor.py +0 -175
- novel_downloader-2.0.0/src/novel_downloader/utils/crypto_utils/rc4.py +0 -64
- novel_downloader-2.0.0/src/novel_downloader/utils/file_utils/io.py +0 -106
- novel_downloader-2.0.0/src/novel_downloader/utils/logger.py +0 -96
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/LICENSE +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/setup.cfg +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/clean.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/config.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/main.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/search.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/config/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/config/file_io.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/deqixs/parser.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/deqixs/searcher.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/qidian/searcher.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/wanbengo/searcher.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/xshbook/searcher.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/signals.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/registry.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/downloader.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/searcher.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/aaatxt.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/biquyuedu.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/dxmwx.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/guidaye.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/hetushu.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/i25zw.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/ixdzs8.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/jpxs123.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/lewenn.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/linovelib.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/piaotia.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/qbtr.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/qianbi.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/quanben5.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/registry.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/sfacg.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/shencou.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/shuhaige.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/tongrenquan.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/ttkan.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/wanbengo.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/xiaoshuowu.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/xs63b.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/xshbook.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/yamibo.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/yibige.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/aaatxt.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/dxmwx.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/esjzone.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/hetushu.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/i25zw.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/ixdzs8.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/jpxs123.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/piaotia.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/qbtr.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/qianbi.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/quanben5.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/registry.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/shuhaige.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/tongrenquan.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/ttkan.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/xiaoshuowu.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/xiguashuwu.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/xs63b.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/login.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/search.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/css_styles/intro.css +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/css_styles/main.css +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/images/volume_border.png +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/json/linovelib_font_map.json +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/json/xiguashuwu.json +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/crypto_utils/aes_v1.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/crypto_utils/aes_v2.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/file_utils/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/i18n.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/state.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/diff_display.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/numeric_conversion.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/time_utils/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/time_utils/datetime_utils.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/components/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/components/navigation.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/pages/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/pages/progress.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/__init__.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/client_dialog.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/cred_broker.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/cred_models.py +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader.egg-info/dependency_links.txt +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader.egg-info/entry_points.txt +0 -0
- {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: novel-downloader
|
3
|
-
Version: 2.0.
|
3
|
+
Version: 2.0.2
|
4
4
|
Summary: A command-line tool for downloading Chinese web novels from Qidian and similar platforms.
|
5
5
|
Author-email: Saudade Z <saudadez217@gmail.com>
|
6
6
|
License: MIT License
|
@@ -47,6 +47,7 @@ Requires-Dist: aiohttp
|
|
47
47
|
Requires-Dist: lxml
|
48
48
|
Requires-Dist: platformdirs
|
49
49
|
Provides-Extra: font-recovery
|
50
|
+
Requires-Dist: brotli; extra == "font-recovery"
|
50
51
|
Requires-Dist: numpy; extra == "font-recovery"
|
51
52
|
Requires-Dist: fonttools; extra == "font-recovery"
|
52
53
|
Requires-Dist: pillow; extra == "font-recovery"
|
@@ -122,6 +123,7 @@ novel-cli download 123456
|
|
122
123
|
|
123
124
|
* 支持站点见: [支持站点列表](https://github.com/saudadez21/novel-downloader/blob/main/docs/4-supported-sites.md)
|
124
125
|
* 更多示例见: [CLI 使用示例](https://github.com/saudadez21/novel-downloader/blob/main/docs/5-cli-usage-examples.md)
|
126
|
+
* 运行中可使用 `CTRL+C` 取消任务
|
125
127
|
|
126
128
|
### 3. 图形界面 (GUI / Web)
|
127
129
|
|
@@ -133,6 +135,8 @@ novel-web
|
|
133
135
|
# novel-web --listen public
|
134
136
|
```
|
135
137
|
|
138
|
+
* 运行中可使用 `CTRL+C` 停止服务
|
139
|
+
|
136
140
|
---
|
137
141
|
|
138
142
|
## 从源码安装 (开发版)
|
@@ -56,6 +56,7 @@ novel-cli download 123456
|
|
56
56
|
|
57
57
|
* 支持站点见: [支持站点列表](https://github.com/saudadez21/novel-downloader/blob/main/docs/4-supported-sites.md)
|
58
58
|
* 更多示例见: [CLI 使用示例](https://github.com/saudadez21/novel-downloader/blob/main/docs/5-cli-usage-examples.md)
|
59
|
+
* 运行中可使用 `CTRL+C` 取消任务
|
59
60
|
|
60
61
|
### 3. 图形界面 (GUI / Web)
|
61
62
|
|
@@ -67,6 +68,8 @@ novel-web
|
|
67
68
|
# novel-web --listen public
|
68
69
|
```
|
69
70
|
|
71
|
+
* 运行中可使用 `CTRL+C` 停止服务
|
72
|
+
|
70
73
|
---
|
71
74
|
|
72
75
|
## 从源码安装 (开发版)
|
@@ -31,6 +31,7 @@ dependencies = [
|
|
31
31
|
|
32
32
|
[project.optional-dependencies]
|
33
33
|
font-recovery = [
|
34
|
+
"brotli",
|
34
35
|
"numpy",
|
35
36
|
"fonttools",
|
36
37
|
"pillow",
|
@@ -110,7 +111,7 @@ testpaths = ["tests"]
|
|
110
111
|
|
111
112
|
[tool.commitizen]
|
112
113
|
name = "cz_conventional_commits"
|
113
|
-
version = "2.0.
|
114
|
+
version = "2.0.2"
|
114
115
|
version_files = [
|
115
116
|
"src/novel_downloader/__init__.py"
|
116
117
|
]
|
@@ -155,7 +155,7 @@ async def _download(
|
|
155
155
|
exporter_cfg = adapter.get_exporter_config()
|
156
156
|
login_cfg = adapter.get_login_config()
|
157
157
|
log_level = adapter.get_log_level()
|
158
|
-
setup_logging(
|
158
|
+
setup_logging(console_level=log_level)
|
159
159
|
|
160
160
|
parser = get_parser(site, parser_cfg)
|
161
161
|
exporter = None
|
@@ -178,7 +178,14 @@ async def _download(
|
|
178
178
|
|
179
179
|
for book in valid_books:
|
180
180
|
ui.info(t("download_downloading", book_id=book["book_id"], site=site))
|
181
|
-
|
181
|
+
|
182
|
+
hook, close = ui.create_progress_hook(
|
183
|
+
prefix=t("download_progress_prefix"), unit="chapters"
|
184
|
+
)
|
185
|
+
try:
|
186
|
+
await downloader.download(book, progress_hook=hook)
|
187
|
+
finally:
|
188
|
+
close()
|
182
189
|
|
183
190
|
if not no_export and exporter is not None:
|
184
191
|
await asyncio.to_thread(exporter.export, book["book_id"])
|
@@ -186,6 +193,9 @@ async def _download(
|
|
186
193
|
if downloader_cfg.login_required and fetcher.is_logged_in:
|
187
194
|
await fetcher.save_state()
|
188
195
|
|
196
|
+
if exporter is not None:
|
197
|
+
exporter.close()
|
198
|
+
|
189
199
|
|
190
200
|
async def _prompt_login_fields(
|
191
201
|
fields: list[LoginField],
|
@@ -214,8 +224,8 @@ async def _prompt_login_fields(
|
|
214
224
|
ui.info(t("login_use_config"))
|
215
225
|
continue
|
216
226
|
|
217
|
-
value: str | dict[str, str]
|
218
|
-
|
227
|
+
value: str | dict[str, str] = ""
|
228
|
+
for _ in range(5):
|
219
229
|
if field.type == "password":
|
220
230
|
value = ui.prompt_password(t("login_enter_password"))
|
221
231
|
elif field.type == "cookie":
|
@@ -235,10 +245,3 @@ async def _prompt_login_fields(
|
|
235
245
|
result[field.name] = value
|
236
246
|
|
237
247
|
return result
|
238
|
-
|
239
|
-
|
240
|
-
async def _print_progress(done: int, total: int) -> None:
|
241
|
-
"""Progress hook passed into the downloader."""
|
242
|
-
ui.print_progress(
|
243
|
-
done, total, prefix=t("download_progress_prefix"), unit="chapters"
|
244
|
-
)
|
@@ -55,22 +55,22 @@ def handle_export(args: Namespace) -> None:
|
|
55
55
|
adapter = ConfigAdapter(config=config_data, site=site)
|
56
56
|
exporter_cfg = adapter.get_exporter_config()
|
57
57
|
log_level = adapter.get_log_level()
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
58
|
+
setup_logging(console_level=log_level)
|
59
|
+
|
60
|
+
with get_exporter(site, exporter_cfg) as exporter:
|
61
|
+
for book_id in book_ids:
|
62
|
+
ui.info(t("export_processing", book_id=book_id, format=export_format))
|
63
|
+
|
64
|
+
if export_format in {"txt", "all"}:
|
65
|
+
try:
|
66
|
+
exporter.export_as_txt(book_id)
|
67
|
+
ui.success(t("export_success_txt", book_id=book_id))
|
68
|
+
except Exception as e:
|
69
|
+
ui.error(t("export_failed_txt", book_id=book_id, err=str(e)))
|
70
|
+
|
71
|
+
if export_format in {"epub", "all"}:
|
72
|
+
try:
|
73
|
+
exporter.export_as_epub(book_id)
|
74
|
+
ui.success(t("export_success_epub", book_id=book_id))
|
75
|
+
except Exception as e:
|
76
|
+
ui.error(t("export_failed_epub", book_id=book_id, err=str(e)))
|
@@ -7,19 +7,20 @@ A small set of Rich-based helpers to keep CLI presentation and prompts
|
|
7
7
|
consistent across subcommands.
|
8
8
|
|
9
9
|
Public API:
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
* info, success, warn, error
|
11
|
+
* confirm
|
12
|
+
* prompt, prompt_password
|
13
|
+
* render_table
|
14
|
+
* select_index
|
15
|
+
* print_progress
|
16
16
|
"""
|
17
17
|
|
18
18
|
from __future__ import annotations
|
19
19
|
|
20
|
-
from collections.abc import Iterable, Sequence
|
20
|
+
from collections.abc import Awaitable, Callable, Iterable, Sequence
|
21
21
|
|
22
22
|
from rich.console import Console
|
23
|
+
from rich.progress import Progress, TaskID
|
23
24
|
from rich.prompt import Confirm, Prompt
|
24
25
|
from rich.table import Table
|
25
26
|
|
@@ -71,7 +72,7 @@ def prompt(message: str, *, default: str | None = None) -> str:
|
|
71
72
|
:return: The user's input.
|
72
73
|
"""
|
73
74
|
try:
|
74
|
-
result: str = Prompt.ask(message, default=default or "")
|
75
|
+
result: str = Prompt.ask(message, default=default or "", show_default=False)
|
75
76
|
return result
|
76
77
|
except (KeyboardInterrupt, EOFError):
|
77
78
|
warn("Cancelled.")
|
@@ -154,3 +155,29 @@ def print_progress(
|
|
154
155
|
total = max(1, total)
|
155
156
|
pct = done / total * 100.0
|
156
157
|
_CONSOLE.print(f"[dim]{prefix}[/] {done}/{total} {unit} ({pct:.2f}%)")
|
158
|
+
|
159
|
+
|
160
|
+
def create_progress_hook(
|
161
|
+
prefix: str = "Progress",
|
162
|
+
unit: str = "item",
|
163
|
+
) -> tuple[Callable[[int, int], Awaitable[None]], Callable[[], None]]:
|
164
|
+
progress = Progress(console=_CONSOLE)
|
165
|
+
task_id: TaskID | None = None
|
166
|
+
|
167
|
+
async def hook(done: int, total: int) -> None:
|
168
|
+
nonlocal task_id
|
169
|
+
if task_id is None:
|
170
|
+
progress.start()
|
171
|
+
task_id = progress.add_task(f"[cyan]{prefix}[/]", total=max(1, total))
|
172
|
+
|
173
|
+
progress.update(
|
174
|
+
task_id,
|
175
|
+
completed=done,
|
176
|
+
total=max(1, total),
|
177
|
+
description=f"{prefix} ({done}/{total} {unit})",
|
178
|
+
)
|
179
|
+
|
180
|
+
def close() -> None:
|
181
|
+
progress.stop()
|
182
|
+
|
183
|
+
return hook, close
|
@@ -0,0 +1,383 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.config.adapter
|
4
|
+
-------------------------------
|
5
|
+
|
6
|
+
Defines ConfigAdapter, which maps a raw configuration dictionary and
|
7
|
+
site into structured dataclass-based config models.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import contextlib
|
11
|
+
import json
|
12
|
+
from collections.abc import Mapping
|
13
|
+
from typing import Any, TypeVar
|
14
|
+
|
15
|
+
from novel_downloader.models import (
|
16
|
+
BookConfig,
|
17
|
+
DownloaderConfig,
|
18
|
+
ExporterConfig,
|
19
|
+
FetcherConfig,
|
20
|
+
FontOCRConfig,
|
21
|
+
ParserConfig,
|
22
|
+
TextCleanerConfig,
|
23
|
+
)
|
24
|
+
|
25
|
+
T = TypeVar("T")
|
26
|
+
|
27
|
+
|
28
|
+
class ConfigAdapter:
|
29
|
+
"""
|
30
|
+
Adapter to map a raw configuration dictionary and site name
|
31
|
+
into structured dataclass configuration models.
|
32
|
+
|
33
|
+
Resolution order for each field:
|
34
|
+
1. ``config["sites"][<site>]`` (if present)
|
35
|
+
2. ``config["general"]`` (if present)
|
36
|
+
3. Hard-coded default passed by the caller
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(self, config: Mapping[str, Any], site: str):
|
40
|
+
"""
|
41
|
+
Initialize the adapter with a configuration mapping and a site key.
|
42
|
+
|
43
|
+
:param config: Fully loaded configuration mapping.
|
44
|
+
:param site: Current site key (e.g., ``"qidian"``).
|
45
|
+
"""
|
46
|
+
self._config: dict[str, Any] = dict(config)
|
47
|
+
self._site: str = site
|
48
|
+
|
49
|
+
def get_fetcher_config(self) -> FetcherConfig:
|
50
|
+
"""
|
51
|
+
Build a :class:`novel_downloader.models.FetcherConfig` by resolving fields
|
52
|
+
from site-specific and general settings.
|
53
|
+
|
54
|
+
:return: Fully populated configuration for the network fetcher.
|
55
|
+
"""
|
56
|
+
s, g = self._site_cfg, self._gen_cfg
|
57
|
+
return FetcherConfig(
|
58
|
+
request_interval=self._pick("request_interval", 2.0, s, g),
|
59
|
+
retry_times=self._pick("retry_times", 3, s, g),
|
60
|
+
backoff_factor=self._pick("backoff_factor", 2.0, s, g),
|
61
|
+
timeout=self._pick("timeout", 30.0, s, g),
|
62
|
+
max_connections=self._pick("max_connections", 10, s, g),
|
63
|
+
max_rps=self._pick("max_rps", 1000.0, s, g),
|
64
|
+
user_agent=self._pick("user_agent", None, s, g),
|
65
|
+
headers=self._pick("headers", None, s, g),
|
66
|
+
verify_ssl=self._pick("verify_ssl", True, s, g),
|
67
|
+
locale_style=self._pick("locale_style", "simplified", s, g),
|
68
|
+
)
|
69
|
+
|
70
|
+
def get_downloader_config(self) -> DownloaderConfig:
|
71
|
+
"""
|
72
|
+
Build a :class:`novel_downloader.models.DownloaderConfig` using both
|
73
|
+
general and site-specific settings.
|
74
|
+
|
75
|
+
:return: Fully populated configuration for the chapter/page downloader.
|
76
|
+
"""
|
77
|
+
s, g = self._site_cfg, self._gen_cfg
|
78
|
+
debug = g.get("debug") or {}
|
79
|
+
return DownloaderConfig(
|
80
|
+
request_interval=self._pick("request_interval", 2.0, s, g),
|
81
|
+
retry_times=self._pick("retry_times", 3, s, g),
|
82
|
+
backoff_factor=self._pick("backoff_factor", 2.0, s, g),
|
83
|
+
workers=self._pick("workers", 2, s, g),
|
84
|
+
skip_existing=self._pick("skip_existing", True, s, g),
|
85
|
+
login_required=bool(s.get("login_required", False)),
|
86
|
+
save_html=bool(debug.get("save_html", False)),
|
87
|
+
raw_data_dir=g.get("raw_data_dir", "./raw_data"),
|
88
|
+
cache_dir=g.get("cache_dir", "./novel_cache"),
|
89
|
+
storage_batch_size=g.get("storage_batch_size", 1),
|
90
|
+
)
|
91
|
+
|
92
|
+
def get_parser_config(self) -> ParserConfig:
|
93
|
+
"""
|
94
|
+
Build a :class:`novel_downloader.models.ParserConfig` from general,
|
95
|
+
OCR-related, and site-specific settings.
|
96
|
+
|
97
|
+
:return: Fully populated configuration for the parser stage.
|
98
|
+
"""
|
99
|
+
g = self._gen_cfg
|
100
|
+
s = self._site_cfg
|
101
|
+
g_font = g.get("font_ocr") or {}
|
102
|
+
s_font = s.get("font_ocr") or {}
|
103
|
+
font_ocr: dict[str, Any] = {**g_font, **s_font}
|
104
|
+
return ParserConfig(
|
105
|
+
cache_dir=g.get("cache_dir", "./novel_cache"),
|
106
|
+
use_truncation=bool(s.get("use_truncation", True)),
|
107
|
+
decode_font=bool(font_ocr.get("decode_font", False)),
|
108
|
+
save_font_debug=bool(font_ocr.get("save_font_debug", False)),
|
109
|
+
batch_size=int(font_ocr.get("batch_size", 32)),
|
110
|
+
fontocr_cfg=self._dict_to_fontocr_cfg(font_ocr),
|
111
|
+
)
|
112
|
+
|
113
|
+
def get_exporter_config(self) -> ExporterConfig:
|
114
|
+
"""
|
115
|
+
Build an :class:`novel_downloader.models.ExporterConfig` from the
|
116
|
+
``output`` and ``cleaner`` sections plus general settings.
|
117
|
+
|
118
|
+
:return: Fully populated configuration for text/ebook export.
|
119
|
+
"""
|
120
|
+
g = self._gen_cfg
|
121
|
+
out = self._config.get("output") or {}
|
122
|
+
cln = self._config.get("cleaner") or {}
|
123
|
+
fmt = out.get("formats") or {}
|
124
|
+
naming = out.get("naming") or {}
|
125
|
+
epub_opts = out.get("epub") or {}
|
126
|
+
|
127
|
+
cleaner_cfg = self._dict_to_cleaner_cfg(cln)
|
128
|
+
return ExporterConfig(
|
129
|
+
cache_dir=g.get("cache_dir", "./novel_cache"),
|
130
|
+
raw_data_dir=g.get("raw_data_dir", "./raw_data"),
|
131
|
+
output_dir=g.get("output_dir", "./downloads"),
|
132
|
+
clean_text=cln.get("clean_text", False),
|
133
|
+
make_txt=fmt.get("make_txt", True),
|
134
|
+
make_epub=fmt.get("make_epub", True),
|
135
|
+
make_md=fmt.get("make_md", False),
|
136
|
+
make_pdf=fmt.get("make_pdf", False),
|
137
|
+
append_timestamp=naming.get("append_timestamp", True),
|
138
|
+
filename_template=naming.get("filename_template", "{title}_{author}"),
|
139
|
+
include_cover=epub_opts.get("include_cover", True),
|
140
|
+
include_picture=epub_opts.get("include_picture", True),
|
141
|
+
split_mode=self._site_cfg.get("split_mode", "book"),
|
142
|
+
cleaner_cfg=cleaner_cfg,
|
143
|
+
)
|
144
|
+
|
145
|
+
def get_login_config(self) -> dict[str, str]:
|
146
|
+
"""
|
147
|
+
Extract login-related fields from the current site configuration.
|
148
|
+
Only non-empty string values are returned; values are stripped.
|
149
|
+
|
150
|
+
:return: A subset of ``{"username","password","cookies"}`` that are non-empty
|
151
|
+
"""
|
152
|
+
out: dict[str, str] = {}
|
153
|
+
for key in ("username", "password", "cookies"):
|
154
|
+
val = self._site_cfg.get(key, "")
|
155
|
+
if isinstance(val, str):
|
156
|
+
s = val.strip()
|
157
|
+
if s:
|
158
|
+
out[key] = s
|
159
|
+
return out
|
160
|
+
|
161
|
+
def get_book_ids(self) -> list[BookConfig]:
|
162
|
+
"""
|
163
|
+
Extract and normalize the list of target books for the current site.
|
164
|
+
|
165
|
+
Accepted shapes for ``site.book_ids``:
|
166
|
+
* a single ``str`` or ``int`` (book id)
|
167
|
+
* a dict with fields: book_id and optional start_id, end_id, ignore_ids
|
168
|
+
* a ``list`` containing any mix of the above
|
169
|
+
|
170
|
+
:return: Normalized list of :class:`BookConfig`-compatible dictionaries.
|
171
|
+
:raises ValueError: If ``book_ids`` is neither a scalar ``str|int``, ``dict``,
|
172
|
+
nor ``list``.
|
173
|
+
"""
|
174
|
+
raw = self._site_cfg.get("book_ids", [])
|
175
|
+
|
176
|
+
if isinstance(raw, (str | int)):
|
177
|
+
return [{"book_id": str(raw)}]
|
178
|
+
|
179
|
+
if isinstance(raw, dict):
|
180
|
+
return [self._dict_to_book_cfg(raw)]
|
181
|
+
|
182
|
+
if not isinstance(raw, list):
|
183
|
+
raise ValueError(
|
184
|
+
f"book_ids must be a list or string, got {type(raw).__name__}"
|
185
|
+
)
|
186
|
+
|
187
|
+
result: list[BookConfig] = []
|
188
|
+
for item in raw:
|
189
|
+
try:
|
190
|
+
if isinstance(item, (str | int)):
|
191
|
+
result.append({"book_id": str(item)})
|
192
|
+
elif isinstance(item, dict):
|
193
|
+
result.append(self._dict_to_book_cfg(item))
|
194
|
+
except ValueError:
|
195
|
+
continue
|
196
|
+
return result
|
197
|
+
|
198
|
+
def get_log_level(self) -> str:
|
199
|
+
"""
|
200
|
+
Retrieve the logging level from ``general.debug``.
|
201
|
+
|
202
|
+
:return: One of ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, ``"ERROR"``
|
203
|
+
"""
|
204
|
+
debug_cfg = self._gen_cfg.get("debug", {})
|
205
|
+
return debug_cfg.get("log_level") or "INFO"
|
206
|
+
|
207
|
+
@property
|
208
|
+
def site(self) -> str:
|
209
|
+
return self._site
|
210
|
+
|
211
|
+
@site.setter
|
212
|
+
def site(self, value: str) -> None:
|
213
|
+
self._site = value
|
214
|
+
|
215
|
+
@property
|
216
|
+
def _gen_cfg(self) -> dict[str, Any]:
|
217
|
+
"""
|
218
|
+
A read-only view of the global ``general`` settings.
|
219
|
+
|
220
|
+
:return: ``config["general"]`` if present, else ``{}``.
|
221
|
+
"""
|
222
|
+
return self._config.get("general") or {}
|
223
|
+
|
224
|
+
@property
|
225
|
+
def _site_cfg(self) -> dict[str, Any]:
|
226
|
+
"""
|
227
|
+
Retrieve the configuration block for the current site.
|
228
|
+
|
229
|
+
Lookup order:
|
230
|
+
1. If a site-specific entry exists under ``config["sites"]``, return it.
|
231
|
+
2. Otherwise, if ``config["sites"]["common"]`` exists, return it.
|
232
|
+
3. Else return an empty dict.
|
233
|
+
|
234
|
+
:return: Site-specific mapping, common mapping, or ``{}``.
|
235
|
+
"""
|
236
|
+
sites_cfg = self._config.get("sites") or {}
|
237
|
+
if self._site in sites_cfg and isinstance(sites_cfg[self._site], dict):
|
238
|
+
return sites_cfg[self._site] or {}
|
239
|
+
return sites_cfg.get("common") or {}
|
240
|
+
|
241
|
+
@staticmethod
|
242
|
+
def _has_key(d: Mapping[str, Any] | None, key: str) -> bool:
|
243
|
+
"""
|
244
|
+
Check whether a mapping contains a key.
|
245
|
+
|
246
|
+
:param d: Mapping to inspect.
|
247
|
+
:param key: Key to look up.
|
248
|
+
:return: ``True`` if ``d`` is a Mapping and contains key; otherwise ``False``.
|
249
|
+
"""
|
250
|
+
return isinstance(d, Mapping) and (key in d)
|
251
|
+
|
252
|
+
def _pick(self, key: str, default: T, *sources: Mapping[str, Any]) -> T:
|
253
|
+
"""
|
254
|
+
Resolve ``key`` from the provided ``sources`` in order of precedence.
|
255
|
+
|
256
|
+
:param key: Configuration key to resolve.
|
257
|
+
:param default: Fallback value if ``key`` is absent in all sources.
|
258
|
+
:param sources: One or more mappings to check, in order of precedence.
|
259
|
+
:return: The first present value for ``key``, otherwise ``default``.
|
260
|
+
"""
|
261
|
+
for src in sources:
|
262
|
+
if self._has_key(src, key):
|
263
|
+
return src[key] # type: ignore[no-any-return]
|
264
|
+
return default
|
265
|
+
|
266
|
+
@staticmethod
|
267
|
+
def _dict_to_book_cfg(data: dict[str, Any]) -> BookConfig:
|
268
|
+
"""
|
269
|
+
Convert a raw dict into a :class:`novel_downloader.models.BookConfig`
|
270
|
+
with normalized types (all IDs coerced to strings).
|
271
|
+
|
272
|
+
:param data: A dict that must contain at least "book_id".
|
273
|
+
:return: Normalized :class:`BookConfig` mapping.
|
274
|
+
:raises ValueError: If ``"book_id"`` is missing.
|
275
|
+
"""
|
276
|
+
if "book_id" not in data:
|
277
|
+
raise ValueError("Missing required field 'book_id'")
|
278
|
+
|
279
|
+
out: BookConfig = {"book_id": str(data["book_id"])}
|
280
|
+
|
281
|
+
if "start_id" in data:
|
282
|
+
out["start_id"] = str(data["start_id"])
|
283
|
+
if "end_id" in data:
|
284
|
+
out["end_id"] = str(data["end_id"])
|
285
|
+
if "ignore_ids" in data:
|
286
|
+
with contextlib.suppress(Exception):
|
287
|
+
out["ignore_ids"] = [str(x) for x in data["ignore_ids"]]
|
288
|
+
return out
|
289
|
+
|
290
|
+
@staticmethod
|
291
|
+
def _dict_to_fontocr_cfg(data: dict[str, Any]) -> FontOCRConfig:
|
292
|
+
"""
|
293
|
+
Convert a raw ``font_ocr`` dict into a :class:`FontOCRConfig`.
|
294
|
+
"""
|
295
|
+
if not isinstance(data, dict):
|
296
|
+
return FontOCRConfig()
|
297
|
+
|
298
|
+
ishape = data.get("input_shape")
|
299
|
+
if isinstance(ishape, list):
|
300
|
+
ishape = tuple(ishape) # [C, H, W] -> (C, H, W)
|
301
|
+
|
302
|
+
return FontOCRConfig(
|
303
|
+
model_name=data.get("model_name"),
|
304
|
+
model_dir=data.get("model_dir"),
|
305
|
+
input_shape=ishape,
|
306
|
+
device=data.get("device"),
|
307
|
+
precision=data.get("precision", "fp32"),
|
308
|
+
cpu_threads=data.get("cpu_threads", 10),
|
309
|
+
enable_hpi=data.get("enable_hpi", False),
|
310
|
+
)
|
311
|
+
|
312
|
+
@classmethod
|
313
|
+
def _dict_to_cleaner_cfg(cls, cfg: dict[str, Any]) -> TextCleanerConfig:
|
314
|
+
"""
|
315
|
+
Convert a nested ``cleaner`` block into a
|
316
|
+
:class:`novel_downloader.models.TextCleanerConfig`.
|
317
|
+
|
318
|
+
:param cfg: configuration dictionary
|
319
|
+
:return: Aggregated title/content rules with external file contents merged
|
320
|
+
"""
|
321
|
+
t_remove, t_replace = cls._merge_rules(cfg.get("title", {}) or {})
|
322
|
+
c_remove, c_replace = cls._merge_rules(cfg.get("content", {}) or {})
|
323
|
+
return TextCleanerConfig(
|
324
|
+
remove_invisible=cfg.get("remove_invisible", True),
|
325
|
+
title_remove_patterns=t_remove,
|
326
|
+
title_replacements=t_replace,
|
327
|
+
content_remove_patterns=c_remove,
|
328
|
+
content_replacements=c_replace,
|
329
|
+
)
|
330
|
+
|
331
|
+
@classmethod
|
332
|
+
def _merge_rules(cls, section: dict[str, Any]) -> tuple[list[str], dict[str, str]]:
|
333
|
+
"""
|
334
|
+
Merge inline patterns/replacements with any enabled external files.
|
335
|
+
|
336
|
+
:param section: Mapping describing either the ``title`` or ``content`` rules.
|
337
|
+
:return: Tuple ``(remove_patterns, replace)`` after merging.
|
338
|
+
"""
|
339
|
+
remove = list(section.get("remove_patterns") or [])
|
340
|
+
replace = dict(section.get("replace") or {})
|
341
|
+
ext = section.get("external") or {}
|
342
|
+
if ext.get("enabled", False):
|
343
|
+
rm_path = ext.get("remove_patterns") or ""
|
344
|
+
rp_path = ext.get("replace") or ""
|
345
|
+
remove += cls._load_str_list(rm_path)
|
346
|
+
replace.update(cls._load_str_dict(rp_path))
|
347
|
+
return remove, replace
|
348
|
+
|
349
|
+
@staticmethod
|
350
|
+
def _load_str_list(path: str) -> list[str]:
|
351
|
+
"""
|
352
|
+
Load a JSON file containing a list of strings.
|
353
|
+
|
354
|
+
:param path: File path to a JSON array (e.g., ``["a", "b"]``).
|
355
|
+
:return: Parsed list on success; empty list if ``path`` is empty, file is
|
356
|
+
missing, or content is invalid.
|
357
|
+
"""
|
358
|
+
if not path:
|
359
|
+
return []
|
360
|
+
try:
|
361
|
+
with open(path, encoding="utf-8") as f:
|
362
|
+
data = json.load(f)
|
363
|
+
return list(data) if isinstance(data, list) else []
|
364
|
+
except Exception:
|
365
|
+
return []
|
366
|
+
|
367
|
+
@staticmethod
|
368
|
+
def _load_str_dict(path: str) -> dict[str, str]:
|
369
|
+
"""
|
370
|
+
Load a JSON file containing a dict of string-to-string mappings.
|
371
|
+
|
372
|
+
:param path: File path to a JSON object (e.g., ``{"old":"new"}``).
|
373
|
+
:return: Parsed dict on success; empty dict if ``path`` is empty, file is
|
374
|
+
missing, or content is invalid.
|
375
|
+
"""
|
376
|
+
if not path:
|
377
|
+
return {}
|
378
|
+
try:
|
379
|
+
with open(path, encoding="utf-8") as f:
|
380
|
+
data = json.load(f)
|
381
|
+
return dict(data) if isinstance(data, dict) else {}
|
382
|
+
except Exception:
|
383
|
+
return {}
|
@@ -7,12 +7,11 @@ This package serves as the core layer of the novel_downloader system.
|
|
7
7
|
|
8
8
|
It provides factory methods for constructing key components required for
|
9
9
|
downloading and processing online novel content, including:
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
- search: Provides unified search functionality across supported novel sites.
|
10
|
+
* Downloader: Handles the full download lifecycle of a book or a batch of books.
|
11
|
+
* Parser: Extracts structured data from HTML or SSR content.
|
12
|
+
* Fetcher: Sends HTTP requests and manages sessions, including login if required.
|
13
|
+
* Exporter: Responsible for exporting downloaded data into various output formats.
|
14
|
+
* search: Provides unified search functionality across supported novel sites.
|
16
15
|
"""
|
17
16
|
|
18
17
|
__all__ = [
|