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.
Files changed (236) hide show
  1. {novel_downloader-2.0.0/src/novel_downloader.egg-info → novel_downloader-2.0.2}/PKG-INFO +5 -1
  2. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/README.md +3 -0
  3. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/pyproject.toml +2 -1
  4. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/__init__.py +1 -1
  5. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/download.py +14 -11
  6. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/export.py +19 -19
  7. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/ui.py +35 -8
  8. novel_downloader-2.0.2/src/novel_downloader/config/adapter.py +383 -0
  9. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/__init__.py +5 -6
  10. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/deqixs/fetcher.py +1 -28
  11. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/__init__.py +2 -0
  12. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/base.py +34 -85
  13. novel_downloader-2.0.2/src/novel_downloader/core/downloaders/common.py +257 -0
  14. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/qianbi.py +30 -64
  15. novel_downloader-2.0.2/src/novel_downloader/core/downloaders/qidian.py +309 -0
  16. novel_downloader-2.0.2/src/novel_downloader/core/downloaders/qqbook.py +292 -0
  17. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/registry.py +2 -2
  18. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/exporters/__init__.py +2 -0
  19. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/exporters/base.py +37 -59
  20. novel_downloader-2.0.2/src/novel_downloader/core/exporters/common.py +620 -0
  21. novel_downloader-2.0.2/src/novel_downloader/core/exporters/linovelib.py +47 -0
  22. novel_downloader-2.0.2/src/novel_downloader/core/exporters/qidian.py +61 -0
  23. novel_downloader-2.0.2/src/novel_downloader/core/exporters/qqbook.py +28 -0
  24. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/exporters/registry.py +2 -2
  25. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/__init__.py +4 -2
  26. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/aaatxt.py +2 -22
  27. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/b520.py +3 -23
  28. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/base.py +80 -105
  29. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/biquyuedu.py +2 -22
  30. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/dxmwx.py +10 -22
  31. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/esjzone.py +6 -29
  32. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/guidaye.py +2 -22
  33. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/hetushu.py +9 -29
  34. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/i25zw.py +2 -16
  35. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/ixdzs8.py +2 -16
  36. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/jpxs123.py +2 -16
  37. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/lewenn.py +2 -22
  38. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/linovelib.py +4 -20
  39. 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
  40. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/piaotia.py +2 -16
  41. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/qbtr.py +2 -16
  42. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/qianbi.py +1 -20
  43. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/qidian.py +27 -68
  44. novel_downloader-2.0.2/src/novel_downloader/core/fetchers/qqbook.py +177 -0
  45. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/quanben5.py +9 -29
  46. novel_downloader-2.0.2/src/novel_downloader/core/fetchers/rate_limiter.py +55 -0
  47. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/sfacg.py +3 -16
  48. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/shencou.py +2 -16
  49. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/shuhaige.py +2 -22
  50. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/tongrenquan.py +2 -22
  51. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/ttkan.py +3 -14
  52. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/wanbengo.py +2 -22
  53. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/xiaoshuowu.py +2 -16
  54. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/xiguashuwu.py +4 -20
  55. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/xs63b.py +3 -15
  56. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/xshbook.py +2 -22
  57. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/yamibo.py +4 -28
  58. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/yibige.py +13 -26
  59. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/exporter.py +19 -7
  60. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/fetcher.py +23 -49
  61. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/parser.py +2 -2
  62. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/__init__.py +4 -2
  63. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/b520.py +2 -2
  64. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/base.py +5 -39
  65. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/esjzone.py +3 -3
  66. 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
  67. novel_downloader-2.0.2/src/novel_downloader/core/parsers/qidian.py +717 -0
  68. novel_downloader-2.0.2/src/novel_downloader/core/parsers/qqbook.py +709 -0
  69. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/xiguashuwu.py +8 -15
  70. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/__init__.py +2 -2
  71. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/b520.py +1 -1
  72. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/base.py +2 -2
  73. 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
  74. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/locales/en.json +3 -3
  75. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/locales/zh.json +3 -3
  76. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/__init__.py +2 -0
  77. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/book.py +1 -0
  78. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/config.py +12 -0
  79. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/config/settings.toml +23 -5
  80. novel_downloader-2.0.2/src/novel_downloader/resources/js_scripts/expr_to_json.js +14 -0
  81. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/js_scripts/qidian_decrypt_node.js +21 -16
  82. novel_downloader-2.0.2/src/novel_downloader/resources/js_scripts/qq_decrypt_node.js +92 -0
  83. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/__init__.py +0 -2
  84. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/chapter_storage.py +2 -3
  85. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/constants.py +7 -3
  86. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/cookies.py +32 -17
  87. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/crypto_utils/__init__.py +0 -6
  88. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/crypto_utils/aes_util.py +1 -1
  89. novel_downloader-2.0.2/src/novel_downloader/utils/crypto_utils/rc4.py +54 -0
  90. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/__init__.py +2 -3
  91. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/builder.py +6 -6
  92. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/constants.py +1 -6
  93. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/documents.py +7 -7
  94. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/models.py +8 -8
  95. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/epub/utils.py +10 -10
  96. novel_downloader-2.0.2/src/novel_downloader/utils/file_utils/io.py +81 -0
  97. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/file_utils/normalize.py +1 -7
  98. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/file_utils/sanitize.py +4 -11
  99. novel_downloader-2.0.2/src/novel_downloader/utils/fontocr/__init__.py +13 -0
  100. 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
  101. novel_downloader-2.0.2/src/novel_downloader/utils/fontocr/loader.py +52 -0
  102. novel_downloader-2.0.2/src/novel_downloader/utils/logger.py +120 -0
  103. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/network.py +16 -40
  104. novel_downloader-2.0.2/src/novel_downloader/utils/node_decryptor/__init__.py +13 -0
  105. novel_downloader-2.0.2/src/novel_downloader/utils/node_decryptor/decryptor.py +342 -0
  106. {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
  107. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/text_cleaner.py +39 -30
  108. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/truncate_utils.py +3 -14
  109. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/time_utils/sleep_utils.py +53 -43
  110. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/main.py +1 -1
  111. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/pages/download.py +1 -1
  112. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/pages/search.py +4 -4
  113. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/task_manager.py +2 -0
  114. {novel_downloader-2.0.0 → novel_downloader-2.0.2/src/novel_downloader.egg-info}/PKG-INFO +5 -1
  115. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader.egg-info/SOURCES.txt +18 -25
  116. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader.egg-info/requires.txt +1 -0
  117. novel_downloader-2.0.0/src/novel_downloader/config/adapter.py +0 -320
  118. novel_downloader-2.0.0/src/novel_downloader/core/downloaders/common.py +0 -281
  119. novel_downloader-2.0.0/src/novel_downloader/core/downloaders/qidian.py +0 -336
  120. novel_downloader-2.0.0/src/novel_downloader/core/exporters/common/__init__.py +0 -11
  121. novel_downloader-2.0.0/src/novel_downloader/core/exporters/common/epub.py +0 -198
  122. novel_downloader-2.0.0/src/novel_downloader/core/exporters/common/main_exporter.py +0 -64
  123. novel_downloader-2.0.0/src/novel_downloader/core/exporters/common/txt.py +0 -146
  124. novel_downloader-2.0.0/src/novel_downloader/core/exporters/epub_util.py +0 -215
  125. novel_downloader-2.0.0/src/novel_downloader/core/exporters/linovelib/__init__.py +0 -11
  126. novel_downloader-2.0.0/src/novel_downloader/core/exporters/linovelib/epub.py +0 -349
  127. novel_downloader-2.0.0/src/novel_downloader/core/exporters/linovelib/main_exporter.py +0 -66
  128. novel_downloader-2.0.0/src/novel_downloader/core/exporters/linovelib/txt.py +0 -139
  129. novel_downloader-2.0.0/src/novel_downloader/core/exporters/qidian.py +0 -32
  130. novel_downloader-2.0.0/src/novel_downloader/core/exporters/txt_util.py +0 -67
  131. novel_downloader-2.0.0/src/novel_downloader/core/fetchers/rate_limiter.py +0 -86
  132. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/__init__.py +0 -10
  133. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/book_info_parser.py +0 -89
  134. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -470
  135. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/chapter_normal.py +0 -126
  136. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
  137. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/main_parser.py +0 -101
  138. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/__init__.py +0 -30
  139. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +0 -143
  140. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/helpers.py +0 -110
  141. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/node_decryptor.py +0 -175
  142. novel_downloader-2.0.0/src/novel_downloader/utils/crypto_utils/rc4.py +0 -64
  143. novel_downloader-2.0.0/src/novel_downloader/utils/file_utils/io.py +0 -106
  144. novel_downloader-2.0.0/src/novel_downloader/utils/logger.py +0 -96
  145. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/LICENSE +0 -0
  146. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/setup.cfg +0 -0
  147. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/__init__.py +0 -0
  148. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/clean.py +0 -0
  149. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/config.py +0 -0
  150. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/main.py +0 -0
  151. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/cli/search.py +0 -0
  152. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/config/__init__.py +0 -0
  153. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/config/file_io.py +0 -0
  154. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/deqixs/parser.py +0 -0
  155. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/deqixs/searcher.py +0 -0
  156. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/qidian/searcher.py +0 -0
  157. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/wanbengo/searcher.py +0 -0
  158. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/archived/xshbook/searcher.py +0 -0
  159. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/downloaders/signals.py +0 -0
  160. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/fetchers/registry.py +0 -0
  161. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/__init__.py +0 -0
  162. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/downloader.py +0 -0
  163. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/interfaces/searcher.py +0 -0
  164. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/aaatxt.py +0 -0
  165. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/biquyuedu.py +0 -0
  166. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/dxmwx.py +0 -0
  167. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/guidaye.py +0 -0
  168. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/hetushu.py +0 -0
  169. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/i25zw.py +0 -0
  170. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/ixdzs8.py +0 -0
  171. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/jpxs123.py +0 -0
  172. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/lewenn.py +0 -0
  173. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/linovelib.py +0 -0
  174. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/piaotia.py +0 -0
  175. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/qbtr.py +0 -0
  176. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/qianbi.py +0 -0
  177. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/quanben5.py +0 -0
  178. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/registry.py +0 -0
  179. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/sfacg.py +0 -0
  180. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/shencou.py +0 -0
  181. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/shuhaige.py +0 -0
  182. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/tongrenquan.py +0 -0
  183. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/ttkan.py +0 -0
  184. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/wanbengo.py +0 -0
  185. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/xiaoshuowu.py +0 -0
  186. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/xs63b.py +0 -0
  187. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/xshbook.py +0 -0
  188. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/yamibo.py +0 -0
  189. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/parsers/yibige.py +0 -0
  190. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/aaatxt.py +0 -0
  191. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/dxmwx.py +0 -0
  192. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/esjzone.py +0 -0
  193. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/hetushu.py +0 -0
  194. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/i25zw.py +0 -0
  195. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/ixdzs8.py +0 -0
  196. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/jpxs123.py +0 -0
  197. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/piaotia.py +0 -0
  198. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/qbtr.py +0 -0
  199. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/qianbi.py +0 -0
  200. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/quanben5.py +0 -0
  201. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/registry.py +0 -0
  202. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/shuhaige.py +0 -0
  203. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/tongrenquan.py +0 -0
  204. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/ttkan.py +0 -0
  205. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/xiaoshuowu.py +0 -0
  206. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/xiguashuwu.py +0 -0
  207. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/core/searchers/xs63b.py +0 -0
  208. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/login.py +0 -0
  209. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/models/search.py +0 -0
  210. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/css_styles/intro.css +0 -0
  211. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/css_styles/main.css +0 -0
  212. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/images/volume_border.png +0 -0
  213. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/json/linovelib_font_map.json +0 -0
  214. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/resources/json/xiguashuwu.json +0 -0
  215. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/crypto_utils/aes_v1.py +0 -0
  216. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/crypto_utils/aes_v2.py +0 -0
  217. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/file_utils/__init__.py +0 -0
  218. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/i18n.py +0 -0
  219. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/state.py +0 -0
  220. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/__init__.py +0 -0
  221. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/diff_display.py +0 -0
  222. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/text_utils/numeric_conversion.py +0 -0
  223. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/time_utils/__init__.py +0 -0
  224. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/utils/time_utils/datetime_utils.py +0 -0
  225. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/__init__.py +0 -0
  226. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/components/__init__.py +0 -0
  227. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/components/navigation.py +0 -0
  228. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/pages/__init__.py +0 -0
  229. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/pages/progress.py +0 -0
  230. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/__init__.py +0 -0
  231. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/client_dialog.py +0 -0
  232. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/cred_broker.py +0 -0
  233. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader/web/services/cred_models.py +0 -0
  234. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader.egg-info/dependency_links.txt +0 -0
  235. {novel_downloader-2.0.0 → novel_downloader-2.0.2}/src/novel_downloader.egg-info/entry_points.txt +0 -0
  236. {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.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.0"
114
+ version = "2.0.2"
114
115
  version_files = [
115
116
  "src/novel_downloader/__init__.py"
116
117
  ]
@@ -6,7 +6,7 @@ novel_downloader
6
6
  Core package for the Novel Downloader project.
7
7
  """
8
8
 
9
- __version__ = "2.0.0"
9
+ __version__ = "2.0.2"
10
10
 
11
11
  __author__ = "Saudade Z"
12
12
  __email__ = "saudadez217@gmail.com"
@@ -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(log_level=log_level)
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
- await downloader.download(book, progress_hook=_print_progress)
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
- while True:
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
- exporter = get_exporter(site, exporter_cfg)
59
- setup_logging(log_level=log_level)
60
-
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)))
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
- - info, success, warn, error
11
- - confirm
12
- - prompt, prompt_password
13
- - render_table
14
- - select_index
15
- - print_progress
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
- - Downloader: Handles the full download lifecycle of a book or a batch of books.
12
- - Parser: Extracts structured data from HTML or SSR content.
13
- - Fetcher: Sends HTTP requests and manages sessions, including login if required.
14
- - Exporter: Responsible for exporting downloaded data into various output formats.
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__ = [