novel-downloader 2.0.0__tar.gz → 2.0.1__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 (223) hide show
  1. {novel_downloader-2.0.0/src/novel_downloader.egg-info → novel_downloader-2.0.1}/PKG-INFO +2 -1
  2. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/pyproject.toml +2 -1
  3. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/__init__.py +1 -1
  4. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/cli/download.py +3 -3
  5. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/cli/export.py +1 -1
  6. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/cli/ui.py +7 -7
  7. novel_downloader-2.0.1/src/novel_downloader/config/adapter.py +357 -0
  8. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/__init__.py +5 -6
  9. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/common/txt.py +9 -9
  10. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/linovelib/txt.py +9 -9
  11. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/qidian.py +20 -35
  12. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/interfaces/fetcher.py +2 -2
  13. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/interfaces/parser.py +2 -2
  14. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/base.py +1 -0
  15. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/eightnovel.py +2 -2
  16. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/esjzone.py +3 -3
  17. novel_downloader-2.0.1/src/novel_downloader/core/parsers/qidian/main_parser.py +836 -0
  18. novel_downloader-2.0.1/src/novel_downloader/core/parsers/qidian/utils/__init__.py +11 -0
  19. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
  20. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/xiguashuwu.py +6 -12
  21. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/locales/en.json +3 -3
  22. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/locales/zh.json +3 -3
  23. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/__init__.py +0 -2
  24. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/chapter_storage.py +2 -3
  25. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/constants.py +1 -3
  26. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/cookies.py +32 -17
  27. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/crypto_utils/__init__.py +0 -6
  28. novel_downloader-2.0.1/src/novel_downloader/utils/crypto_utils/rc4.py +54 -0
  29. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/epub/__init__.py +2 -3
  30. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/epub/builder.py +6 -6
  31. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/epub/constants.py +5 -5
  32. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/epub/documents.py +7 -7
  33. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/epub/models.py +8 -8
  34. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/epub/utils.py +10 -10
  35. novel_downloader-2.0.1/src/novel_downloader/utils/file_utils/io.py +81 -0
  36. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/file_utils/normalize.py +1 -7
  37. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/file_utils/sanitize.py +4 -11
  38. novel_downloader-2.0.1/src/novel_downloader/utils/fontocr/__init__.py +13 -0
  39. novel_downloader-2.0.0/src/novel_downloader/utils/fontocr.py → novel_downloader-2.0.1/src/novel_downloader/utils/fontocr/core.py +70 -61
  40. novel_downloader-2.0.1/src/novel_downloader/utils/fontocr/loader.py +50 -0
  41. novel_downloader-2.0.1/src/novel_downloader/utils/logger.py +120 -0
  42. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/network.py +16 -40
  43. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/text_utils/text_cleaner.py +39 -30
  44. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/text_utils/truncate_utils.py +3 -14
  45. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/time_utils/sleep_utils.py +53 -43
  46. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/main.py +1 -1
  47. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/pages/search.py +3 -3
  48. {novel_downloader-2.0.0 → novel_downloader-2.0.1/src/novel_downloader.egg-info}/PKG-INFO +2 -1
  49. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader.egg-info/SOURCES.txt +3 -7
  50. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader.egg-info/requires.txt +1 -0
  51. novel_downloader-2.0.0/src/novel_downloader/config/adapter.py +0 -320
  52. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/book_info_parser.py +0 -89
  53. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -470
  54. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/chapter_normal.py +0 -126
  55. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
  56. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/main_parser.py +0 -101
  57. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/__init__.py +0 -30
  58. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +0 -143
  59. novel_downloader-2.0.0/src/novel_downloader/core/parsers/qidian/utils/helpers.py +0 -110
  60. novel_downloader-2.0.0/src/novel_downloader/utils/crypto_utils/rc4.py +0 -64
  61. novel_downloader-2.0.0/src/novel_downloader/utils/file_utils/io.py +0 -106
  62. novel_downloader-2.0.0/src/novel_downloader/utils/logger.py +0 -96
  63. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/LICENSE +0 -0
  64. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/README.md +0 -0
  65. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/setup.cfg +0 -0
  66. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/cli/__init__.py +0 -0
  67. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/cli/clean.py +0 -0
  68. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/cli/config.py +0 -0
  69. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/cli/main.py +0 -0
  70. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/cli/search.py +0 -0
  71. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/config/__init__.py +0 -0
  72. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/config/file_io.py +0 -0
  73. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/archived/deqixs/fetcher.py +0 -0
  74. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/archived/deqixs/parser.py +0 -0
  75. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/archived/deqixs/searcher.py +0 -0
  76. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/archived/qidian/searcher.py +0 -0
  77. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/archived/wanbengo/searcher.py +0 -0
  78. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/archived/xshbook/searcher.py +0 -0
  79. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/downloaders/__init__.py +0 -0
  80. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/downloaders/base.py +0 -0
  81. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/downloaders/common.py +0 -0
  82. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/downloaders/qianbi.py +0 -0
  83. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/downloaders/qidian.py +0 -0
  84. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/downloaders/registry.py +0 -0
  85. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/downloaders/signals.py +0 -0
  86. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/__init__.py +0 -0
  87. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/base.py +0 -0
  88. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/common/__init__.py +0 -0
  89. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/common/epub.py +0 -0
  90. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/common/main_exporter.py +0 -0
  91. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/epub_util.py +0 -0
  92. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/linovelib/__init__.py +0 -0
  93. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/linovelib/epub.py +0 -0
  94. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/linovelib/main_exporter.py +0 -0
  95. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/qidian.py +0 -0
  96. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/registry.py +0 -0
  97. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/exporters/txt_util.py +0 -0
  98. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/__init__.py +0 -0
  99. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/aaatxt.py +0 -0
  100. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/b520.py +0 -0
  101. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/base.py +0 -0
  102. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/biquyuedu.py +0 -0
  103. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/dxmwx.py +0 -0
  104. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/eightnovel.py +0 -0
  105. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/esjzone.py +0 -0
  106. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/guidaye.py +0 -0
  107. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/hetushu.py +0 -0
  108. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/i25zw.py +0 -0
  109. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/ixdzs8.py +0 -0
  110. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/jpxs123.py +0 -0
  111. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/lewenn.py +0 -0
  112. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/linovelib.py +0 -0
  113. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/piaotia.py +0 -0
  114. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/qbtr.py +0 -0
  115. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/qianbi.py +0 -0
  116. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/quanben5.py +0 -0
  117. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/rate_limiter.py +0 -0
  118. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/registry.py +0 -0
  119. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/sfacg.py +0 -0
  120. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/shencou.py +0 -0
  121. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/shuhaige.py +0 -0
  122. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/tongrenquan.py +0 -0
  123. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/ttkan.py +0 -0
  124. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/wanbengo.py +0 -0
  125. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/xiaoshuowu.py +0 -0
  126. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/xiguashuwu.py +0 -0
  127. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/xs63b.py +0 -0
  128. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/xshbook.py +0 -0
  129. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/yamibo.py +0 -0
  130. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/fetchers/yibige.py +0 -0
  131. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/interfaces/__init__.py +0 -0
  132. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/interfaces/downloader.py +0 -0
  133. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/interfaces/exporter.py +0 -0
  134. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/interfaces/searcher.py +0 -0
  135. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/__init__.py +0 -0
  136. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/aaatxt.py +0 -0
  137. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/b520.py +0 -0
  138. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/biquyuedu.py +0 -0
  139. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/dxmwx.py +0 -0
  140. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/guidaye.py +0 -0
  141. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/hetushu.py +0 -0
  142. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/i25zw.py +0 -0
  143. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/ixdzs8.py +0 -0
  144. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/jpxs123.py +0 -0
  145. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/lewenn.py +0 -0
  146. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/linovelib.py +0 -0
  147. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/piaotia.py +0 -0
  148. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/qbtr.py +0 -0
  149. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/qianbi.py +0 -0
  150. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/qidian/__init__.py +0 -0
  151. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +0 -0
  152. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/quanben5.py +0 -0
  153. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/registry.py +0 -0
  154. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/sfacg.py +0 -0
  155. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/shencou.py +0 -0
  156. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/shuhaige.py +0 -0
  157. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/tongrenquan.py +0 -0
  158. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/ttkan.py +0 -0
  159. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/wanbengo.py +0 -0
  160. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/xiaoshuowu.py +0 -0
  161. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/xs63b.py +0 -0
  162. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/xshbook.py +0 -0
  163. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/yamibo.py +0 -0
  164. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/parsers/yibige.py +0 -0
  165. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/__init__.py +0 -0
  166. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/aaatxt.py +0 -0
  167. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/b520.py +0 -0
  168. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/base.py +0 -0
  169. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/dxmwx.py +0 -0
  170. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/eightnovel.py +0 -0
  171. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/esjzone.py +0 -0
  172. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/hetushu.py +0 -0
  173. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/i25zw.py +0 -0
  174. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/ixdzs8.py +0 -0
  175. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/jpxs123.py +0 -0
  176. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/piaotia.py +0 -0
  177. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/qbtr.py +0 -0
  178. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/qianbi.py +0 -0
  179. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/quanben5.py +0 -0
  180. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/registry.py +0 -0
  181. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/shuhaige.py +0 -0
  182. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/tongrenquan.py +0 -0
  183. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/ttkan.py +0 -0
  184. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/xiaoshuowu.py +0 -0
  185. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/xiguashuwu.py +0 -0
  186. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/core/searchers/xs63b.py +0 -0
  187. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/models/__init__.py +0 -0
  188. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/models/book.py +0 -0
  189. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/models/config.py +0 -0
  190. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/models/login.py +0 -0
  191. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/models/search.py +0 -0
  192. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/resources/config/settings.toml +0 -0
  193. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/resources/css_styles/intro.css +0 -0
  194. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/resources/css_styles/main.css +0 -0
  195. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/resources/images/volume_border.png +0 -0
  196. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/resources/js_scripts/qidian_decrypt_node.js +0 -0
  197. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/resources/json/linovelib_font_map.json +0 -0
  198. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/resources/json/xiguashuwu.json +0 -0
  199. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/crypto_utils/aes_util.py +0 -0
  200. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/crypto_utils/aes_v1.py +0 -0
  201. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/crypto_utils/aes_v2.py +0 -0
  202. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/file_utils/__init__.py +0 -0
  203. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/i18n.py +0 -0
  204. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/state.py +0 -0
  205. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/text_utils/__init__.py +0 -0
  206. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/text_utils/diff_display.py +0 -0
  207. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/text_utils/numeric_conversion.py +0 -0
  208. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/time_utils/__init__.py +0 -0
  209. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/utils/time_utils/datetime_utils.py +0 -0
  210. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/__init__.py +0 -0
  211. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/components/__init__.py +0 -0
  212. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/components/navigation.py +0 -0
  213. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/pages/__init__.py +0 -0
  214. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/pages/download.py +0 -0
  215. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/pages/progress.py +0 -0
  216. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/services/__init__.py +0 -0
  217. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/services/client_dialog.py +0 -0
  218. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/services/cred_broker.py +0 -0
  219. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/services/cred_models.py +0 -0
  220. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader/web/services/task_manager.py +0 -0
  221. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader.egg-info/dependency_links.txt +0 -0
  222. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/src/novel_downloader.egg-info/entry_points.txt +0 -0
  223. {novel_downloader-2.0.0 → novel_downloader-2.0.1}/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.1
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"
@@ -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.1"
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.1"
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
@@ -214,8 +214,8 @@ async def _prompt_login_fields(
214
214
  ui.info(t("login_use_config"))
215
215
  continue
216
216
 
217
- value: str | dict[str, str]
218
- while True:
217
+ value: str | dict[str, str] = ""
218
+ for _ in range(5):
219
219
  if field.type == "password":
220
220
  value = ui.prompt_password(t("login_enter_password"))
221
221
  elif field.type == "cookie":
@@ -56,7 +56,7 @@ def handle_export(args: Namespace) -> None:
56
56
  exporter_cfg = adapter.get_exporter_config()
57
57
  log_level = adapter.get_log_level()
58
58
  exporter = get_exporter(site, exporter_cfg)
59
- setup_logging(log_level=log_level)
59
+ setup_logging(console_level=log_level)
60
60
 
61
61
  for book_id in book_ids:
62
62
  ui.info(t("export_processing", book_id=book_id, format=export_format))
@@ -7,12 +7,12 @@ 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
@@ -71,7 +71,7 @@ def prompt(message: str, *, default: str | None = None) -> str:
71
71
  :return: The user's input.
72
72
  """
73
73
  try:
74
- result: str = Prompt.ask(message, default=default or "")
74
+ result: str = Prompt.ask(message, default=default or "", show_default=False)
75
75
  return result
76
76
  except (KeyboardInterrupt, EOFError):
77
77
  warn("Cancelled.")
@@ -0,0 +1,357 @@
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
+ ParserConfig,
21
+ TextCleanerConfig,
22
+ )
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ class ConfigAdapter:
28
+ """
29
+ Adapter to map a raw configuration dictionary and site name
30
+ into structured dataclass configuration models.
31
+
32
+ Resolution order for each field:
33
+ 1. ``config["sites"][<site>]`` (if present)
34
+ 2. ``config["general"]`` (if present)
35
+ 3. Hard-coded default passed by the caller
36
+ """
37
+
38
+ def __init__(self, config: Mapping[str, Any], site: str):
39
+ """
40
+ Initialize the adapter with a configuration mapping and a site key.
41
+
42
+ :param config: Fully loaded configuration mapping.
43
+ :param site: Current site key (e.g., ``"qidian"``).
44
+ """
45
+ self._config: dict[str, Any] = dict(config)
46
+ self._site: str = site
47
+
48
+ def get_fetcher_config(self) -> FetcherConfig:
49
+ """
50
+ Build a :class:`novel_downloader.models.FetcherConfig` by resolving fields
51
+ from site-specific and general settings.
52
+
53
+ :return: Fully populated configuration for the network fetcher.
54
+ """
55
+ s, g = self._site_cfg, self._gen_cfg
56
+ return FetcherConfig(
57
+ request_interval=self._pick("request_interval", 2.0, s, g),
58
+ retry_times=self._pick("retry_times", 3, s, g),
59
+ backoff_factor=self._pick("backoff_factor", 2.0, s, g),
60
+ timeout=self._pick("timeout", 30.0, s, g),
61
+ max_connections=self._pick("max_connections", 10, s, g),
62
+ max_rps=self._pick("max_rps", 1000.0, s, g),
63
+ user_agent=self._pick("user_agent", None, s, g),
64
+ headers=self._pick("headers", None, s, g),
65
+ verify_ssl=self._pick("verify_ssl", True, s, g),
66
+ locale_style=self._pick("locale_style", "simplified", s, g),
67
+ )
68
+
69
+ def get_downloader_config(self) -> DownloaderConfig:
70
+ """
71
+ Build a :class:`novel_downloader.models.DownloaderConfig` using both
72
+ general and site-specific settings.
73
+
74
+ :return: Fully populated configuration for the chapter/page downloader.
75
+ """
76
+ s, g = self._site_cfg, self._gen_cfg
77
+ debug = g.get("debug") or {}
78
+ return DownloaderConfig(
79
+ request_interval=self._pick("request_interval", 2.0, s, g),
80
+ retry_times=self._pick("retry_times", 3, s, g),
81
+ backoff_factor=self._pick("backoff_factor", 2.0, s, g),
82
+ workers=self._pick("workers", 2, s, g),
83
+ skip_existing=self._pick("skip_existing", True, s, g),
84
+ login_required=bool(s.get("login_required", False)),
85
+ save_html=bool(debug.get("save_html", False)),
86
+ raw_data_dir=g.get("raw_data_dir", "./raw_data"),
87
+ cache_dir=g.get("cache_dir", "./novel_cache"),
88
+ storage_batch_size=g.get("storage_batch_size", 1),
89
+ )
90
+
91
+ def get_parser_config(self) -> ParserConfig:
92
+ """
93
+ Build a :class:`novel_downloader.models.ParserConfig` from general,
94
+ OCR-related, and site-specific settings.
95
+
96
+ :return: Fully populated configuration for the parser stage.
97
+ """
98
+ g = self._gen_cfg
99
+ s = self._site_cfg
100
+ font_ocr = g.get("font_ocr") or {}
101
+ return ParserConfig(
102
+ cache_dir=g.get("cache_dir", "./novel_cache"),
103
+ use_truncation=bool(s.get("use_truncation", True)),
104
+ decode_font=bool(font_ocr.get("decode_font", False)),
105
+ save_font_debug=bool(font_ocr.get("save_font_debug", False)),
106
+ batch_size=int(font_ocr.get("batch_size", 32)),
107
+ )
108
+
109
+ def get_exporter_config(self) -> ExporterConfig:
110
+ """
111
+ Build an :class:`novel_downloader.models.ExporterConfig` from the
112
+ ``output`` and ``cleaner`` sections plus general settings.
113
+
114
+ :return: Fully populated configuration for text/ebook export.
115
+ """
116
+ g = self._gen_cfg
117
+ out = self._config.get("output") or {}
118
+ cln = self._config.get("cleaner") or {}
119
+ fmt = out.get("formats") or {}
120
+ naming = out.get("naming") or {}
121
+ epub_opts = out.get("epub") or {}
122
+
123
+ cleaner_cfg = self._dict_to_cleaner_cfg(cln)
124
+ return ExporterConfig(
125
+ cache_dir=g.get("cache_dir", "./novel_cache"),
126
+ raw_data_dir=g.get("raw_data_dir", "./raw_data"),
127
+ output_dir=g.get("output_dir", "./downloads"),
128
+ clean_text=cln.get("clean_text", False),
129
+ make_txt=fmt.get("make_txt", True),
130
+ make_epub=fmt.get("make_epub", True),
131
+ make_md=fmt.get("make_md", False),
132
+ make_pdf=fmt.get("make_pdf", False),
133
+ append_timestamp=naming.get("append_timestamp", True),
134
+ filename_template=naming.get("filename_template", "{title}_{author}"),
135
+ include_cover=epub_opts.get("include_cover", True),
136
+ include_picture=epub_opts.get("include_picture", True),
137
+ split_mode=self._site_cfg.get("split_mode", "book"),
138
+ cleaner_cfg=cleaner_cfg,
139
+ )
140
+
141
+ def get_login_config(self) -> dict[str, str]:
142
+ """
143
+ Extract login-related fields from the current site configuration.
144
+ Only non-empty string values are returned; values are stripped.
145
+
146
+ :return: A subset of ``{"username","password","cookies"}`` that are non-empty
147
+ """
148
+ out: dict[str, str] = {}
149
+ for key in ("username", "password", "cookies"):
150
+ val = self._site_cfg.get(key, "")
151
+ if isinstance(val, str):
152
+ s = val.strip()
153
+ if s:
154
+ out[key] = s
155
+ return out
156
+
157
+ def get_book_ids(self) -> list[BookConfig]:
158
+ """
159
+ Extract and normalize the list of target books for the current site.
160
+
161
+ Accepted shapes for ``site.book_ids``:
162
+ * a single ``str`` or ``int`` (book id)
163
+ * a dict with fields: book_id and optional start_id, end_id, ignore_ids
164
+ * a ``list`` containing any mix of the above
165
+
166
+ :return: Normalized list of :class:`BookConfig`-compatible dictionaries.
167
+ :raises ValueError: If ``book_ids`` is neither a scalar ``str|int``, ``dict``,
168
+ nor ``list``.
169
+ """
170
+ raw = self._site_cfg.get("book_ids", [])
171
+
172
+ if isinstance(raw, (str | int)):
173
+ return [{"book_id": str(raw)}]
174
+
175
+ if isinstance(raw, dict):
176
+ return [self._dict_to_book_cfg(raw)]
177
+
178
+ if not isinstance(raw, list):
179
+ raise ValueError(
180
+ f"book_ids must be a list or string, got {type(raw).__name__}"
181
+ )
182
+
183
+ result: list[BookConfig] = []
184
+ for item in raw:
185
+ try:
186
+ if isinstance(item, (str | int)):
187
+ result.append({"book_id": str(item)})
188
+ elif isinstance(item, dict):
189
+ result.append(self._dict_to_book_cfg(item))
190
+ except ValueError:
191
+ continue
192
+ return result
193
+
194
+ def get_log_level(self) -> str:
195
+ """
196
+ Retrieve the logging level from ``general.debug``.
197
+
198
+ :return: One of ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, ``"ERROR"``
199
+ """
200
+ debug_cfg = self._gen_cfg.get("debug", {})
201
+ return debug_cfg.get("log_level") or "INFO"
202
+
203
+ @property
204
+ def site(self) -> str:
205
+ return self._site
206
+
207
+ @site.setter
208
+ def site(self, value: str) -> None:
209
+ self._site = value
210
+
211
+ @property
212
+ def _gen_cfg(self) -> dict[str, Any]:
213
+ """
214
+ A read-only view of the global ``general`` settings.
215
+
216
+ :return: ``config["general"]`` if present, else ``{}``.
217
+ """
218
+ return self._config.get("general") or {}
219
+
220
+ @property
221
+ def _site_cfg(self) -> dict[str, Any]:
222
+ """
223
+ Retrieve the configuration block for the current site.
224
+
225
+ Lookup order:
226
+ 1. If a site-specific entry exists under ``config["sites"]``, return it.
227
+ 2. Otherwise, if ``config["sites"]["common"]`` exists, return it.
228
+ 3. Else return an empty dict.
229
+
230
+ :return: Site-specific mapping, common mapping, or ``{}``.
231
+ """
232
+ sites_cfg = self._config.get("sites") or {}
233
+ if self._site in sites_cfg and isinstance(sites_cfg[self._site], dict):
234
+ return sites_cfg[self._site] or {}
235
+ return sites_cfg.get("common") or {}
236
+
237
+ @staticmethod
238
+ def _has_key(d: Mapping[str, Any] | None, key: str) -> bool:
239
+ """
240
+ Check whether a mapping contains a key.
241
+
242
+ :param d: Mapping to inspect.
243
+ :param key: Key to look up.
244
+ :return: ``True`` if ``d`` is a Mapping and contains key; otherwise ``False``.
245
+ """
246
+ return isinstance(d, Mapping) and (key in d)
247
+
248
+ def _pick(self, key: str, default: T, *sources: Mapping[str, Any]) -> T:
249
+ """
250
+ Resolve ``key`` from the provided ``sources`` in order of precedence.
251
+
252
+ :param key: Configuration key to resolve.
253
+ :param default: Fallback value if ``key`` is absent in all sources.
254
+ :param sources: One or more mappings to check, in order of precedence.
255
+ :return: The first present value for ``key``, otherwise ``default``.
256
+ """
257
+ for src in sources:
258
+ if self._has_key(src, key):
259
+ return src[key] # type: ignore[no-any-return]
260
+ return default
261
+
262
+ @staticmethod
263
+ def _dict_to_book_cfg(data: dict[str, Any]) -> BookConfig:
264
+ """
265
+ Convert a raw dict into a :class:`novel_downloader.models.BookConfig`
266
+ with normalized types (all IDs coerced to strings).
267
+
268
+ :param data: A dict that must contain at least "book_id".
269
+ :return: Normalized :class:`BookConfig` mapping.
270
+ :raises ValueError: If ``"book_id"`` is missing.
271
+ """
272
+ if "book_id" not in data:
273
+ raise ValueError("Missing required field 'book_id'")
274
+
275
+ out: BookConfig = {"book_id": str(data["book_id"])}
276
+
277
+ if "start_id" in data:
278
+ out["start_id"] = str(data["start_id"])
279
+ if "end_id" in data:
280
+ out["end_id"] = str(data["end_id"])
281
+ if "ignore_ids" in data:
282
+ with contextlib.suppress(Exception):
283
+ out["ignore_ids"] = [str(x) for x in data["ignore_ids"]]
284
+ return out
285
+
286
+ @classmethod
287
+ def _dict_to_cleaner_cfg(cls, cfg: dict[str, Any]) -> TextCleanerConfig:
288
+ """
289
+ Convert a nested ``cleaner`` block into a
290
+ :class:`novel_downloader.models.TextCleanerConfig`.
291
+
292
+ :param cfg: configuration dictionary
293
+ :return: Aggregated title/content rules with external file contents merged
294
+ """
295
+ t_remove, t_replace = cls._merge_rules(cfg.get("title", {}) or {})
296
+ c_remove, c_replace = cls._merge_rules(cfg.get("content", {}) or {})
297
+ return TextCleanerConfig(
298
+ remove_invisible=cfg.get("remove_invisible", True),
299
+ title_remove_patterns=t_remove,
300
+ title_replacements=t_replace,
301
+ content_remove_patterns=c_remove,
302
+ content_replacements=c_replace,
303
+ )
304
+
305
+ @classmethod
306
+ def _merge_rules(cls, section: dict[str, Any]) -> tuple[list[str], dict[str, str]]:
307
+ """
308
+ Merge inline patterns/replacements with any enabled external files.
309
+
310
+ :param section: Mapping describing either the ``title`` or ``content`` rules.
311
+ :return: Tuple ``(remove_patterns, replace)`` after merging.
312
+ """
313
+ remove = list(section.get("remove_patterns") or [])
314
+ replace = dict(section.get("replace") or {})
315
+ ext = section.get("external") or {}
316
+ if ext.get("enabled", False):
317
+ rm_path = ext.get("remove_patterns") or ""
318
+ rp_path = ext.get("replace") or ""
319
+ remove += cls._load_str_list(rm_path)
320
+ replace.update(cls._load_str_dict(rp_path))
321
+ return remove, replace
322
+
323
+ @staticmethod
324
+ def _load_str_list(path: str) -> list[str]:
325
+ """
326
+ Load a JSON file containing a list of strings.
327
+
328
+ :param path: File path to a JSON array (e.g., ``["a", "b"]``).
329
+ :return: Parsed list on success; empty list if ``path`` is empty, file is
330
+ missing, or content is invalid.
331
+ """
332
+ if not path:
333
+ return []
334
+ try:
335
+ with open(path, encoding="utf-8") as f:
336
+ data = json.load(f)
337
+ return list(data) if isinstance(data, list) else []
338
+ except Exception:
339
+ return []
340
+
341
+ @staticmethod
342
+ def _load_str_dict(path: str) -> dict[str, str]:
343
+ """
344
+ Load a JSON file containing a dict of string-to-string mappings.
345
+
346
+ :param path: File path to a JSON object (e.g., ``{"old":"new"}``).
347
+ :return: Parsed dict on success; empty dict if ``path`` is empty, file is
348
+ missing, or content is invalid.
349
+ """
350
+ if not path:
351
+ return {}
352
+ try:
353
+ with open(path, encoding="utf-8") as f:
354
+ data = json.load(f)
355
+ return dict(data) if isinstance(data, dict) else {}
356
+ except Exception:
357
+ 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__ = [
@@ -133,14 +133,14 @@ def common_export_as_txt(
133
133
  out_path = out_dir / out_name
134
134
 
135
135
  # --- Save final text ---
136
- result = write_file(
137
- content=final_text,
138
- filepath=out_path,
139
- write_mode="w",
140
- on_exist="overwrite",
141
- )
142
- if result:
136
+ try:
137
+ result = write_file(
138
+ content=final_text,
139
+ filepath=out_path,
140
+ on_exist="overwrite",
141
+ )
143
142
  exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
144
- else:
145
- exporter.logger.error("%s Failed to write novel to %s", TAG, out_path)
143
+ except Exception as e:
144
+ exporter.logger.error("%s Failed to write novel to %s: %s", TAG, out_path, e)
145
+ return None
146
146
  return result
@@ -126,14 +126,14 @@ def linovelib_export_as_txt(
126
126
  out_path = out_dir / out_name
127
127
 
128
128
  # --- Save final text ---
129
- result = write_file(
130
- content=final_text,
131
- filepath=out_path,
132
- write_mode="w",
133
- on_exist="overwrite",
134
- )
135
- if result:
129
+ try:
130
+ result = write_file(
131
+ content=final_text,
132
+ filepath=out_path,
133
+ on_exist="overwrite",
134
+ )
136
135
  exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
137
- else:
138
- exporter.logger.error("%s Failed to write novel to %s", TAG, out_path)
136
+ except Exception as e:
137
+ exporter.logger.error("%s Failed to write novel to %s: %s", TAG, out_path, e)
138
+ return None
139
139
  return result
@@ -18,10 +18,8 @@ import aiohttp
18
18
  from novel_downloader.core.fetchers.base import BaseSession
19
19
  from novel_downloader.core.fetchers.registry import register_fetcher
20
20
  from novel_downloader.models import FetcherConfig, LoginField
21
- from novel_downloader.utils import (
22
- async_jitter_sleep,
23
- rc4_crypt,
24
- )
21
+ from novel_downloader.utils import async_jitter_sleep
22
+ from novel_downloader.utils.crypto_utils.rc4 import rc4_init, rc4_stream
25
23
 
26
24
 
27
25
  @register_fetcher(
@@ -54,6 +52,8 @@ class QidianSession(BaseSession):
54
52
  **kwargs: Any,
55
53
  ) -> None:
56
54
  super().__init__("qidian", config, cookies, **kwargs)
55
+ self._s_init = rc4_init(self._d2("dGcwOUl0Myo5aA=="))
56
+ self._cookie_key = self._d("d190c2Zw")
57
57
  self._fp_key = self._d("ZmluZ2VycHJpbnQ=")
58
58
  self._ab_key = self._d("YWJub3JtYWw=")
59
59
  self._ck_key = self._d("Y2hlY2tzdW0=")
@@ -165,12 +165,10 @@ class QidianSession(BaseSession):
165
165
  if self._rate_limiter:
166
166
  await self._rate_limiter.wait()
167
167
 
168
- cookie_key = self._d("d190c2Zw")
169
-
170
168
  for attempt in range(self.retry_times + 1):
171
169
  try:
172
170
  refreshed_token = self._build_payload_token(url)
173
- self.update_cookies({cookie_key: refreshed_token})
171
+ self.update_cookies({self._cookie_key: refreshed_token})
174
172
 
175
173
  async with self.session.get(url, **kwargs) as resp:
176
174
  resp.raise_for_status()
@@ -227,40 +225,30 @@ class QidianSession(BaseSession):
227
225
  """
228
226
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
229
227
 
230
- def _update_fp_val(
231
- self,
232
- *,
233
- key: str = "",
234
- ) -> None:
235
- """"""
236
- enc_token = self._get_cookie_value(self._d("d190c2Zw"))
228
+ def _update_fp_val(self) -> None:
229
+ """
230
+ Decrypt the payload from cookie and update `_fp_val` and `_ab_val`.
231
+ """
232
+ enc_token = self._get_cookie_value(self._cookie_key)
237
233
  if not enc_token:
238
234
  return
239
- if not key:
240
- key = self._get_key()
241
- decrypted_json: str = rc4_crypt(key, enc_token, mode="decrypt")
235
+
236
+ cipher_bytes = base64.b64decode(enc_token)
237
+ plain_bytes = rc4_stream(self._s_init, cipher_bytes)
238
+ decrypted_json = plain_bytes.decode("utf-8", errors="replace")
242
239
  payload: dict[str, Any] = json.loads(decrypted_json)
243
240
  self._fp_val = payload.get(self._fp_key, "")
244
241
  self._ab_val = payload.get(self._ab_key, "0" * 32)
245
242
 
246
- def _build_payload_token(
247
- self,
248
- new_uri: str,
249
- *,
250
- key: str = "",
251
- ) -> str:
243
+ def _build_payload_token(self, new_uri: str) -> str:
252
244
  """
253
245
  Patch a timestamp-bearing token with fresh timing and checksum info.
254
246
 
255
247
  :param new_uri: URI used in checksum generation.
256
- :param key: RC4 key extracted from front-end JavaScript (optional).
257
-
258
248
  :return: Updated token with new timing and checksum values.
259
249
  """
260
250
  if not self._fp_val or not self._ab_val:
261
251
  self._update_fp_val()
262
- if not key:
263
- key = self._get_key()
264
252
 
265
253
  # rebuild timing fields
266
254
  loadts = int(time.time() * 1000) # ms since epoch
@@ -278,9 +266,9 @@ class QidianSession(BaseSession):
278
266
  self._ab_key: self._ab_val,
279
267
  self._ck_key: ck_val,
280
268
  }
281
- return rc4_crypt(
282
- key, json.dumps(new_payload, separators=(",", ":")), mode="encrypt"
283
- )
269
+ plain_bytes = json.dumps(new_payload, separators=(",", ":")).encode("utf-8")
270
+ cipher_bytes = rc4_stream(self._s_init, plain_bytes)
271
+ return base64.b64encode(cipher_bytes).decode("utf-8")
284
272
 
285
273
  async def _check_login_status(self) -> bool:
286
274
  """
@@ -335,8 +323,5 @@ class QidianSession(BaseSession):
335
323
  return base64.b64decode(b).decode()
336
324
 
337
325
  @staticmethod
338
- def _get_key() -> str:
339
- encoded = "Lj1qYxMuaXBjMg=="
340
- decoded = base64.b64decode(encoded)
341
- key = "".join([chr(b ^ 0x5A) for b in decoded])
342
- return key
326
+ def _d2(b: str) -> bytes:
327
+ return base64.b64decode(b)
@@ -16,8 +16,8 @@ from novel_downloader.models import LoginField
16
16
  class FetcherProtocol(Protocol):
17
17
  """
18
18
  An async requester must be able to fetch raw HTML/data for:
19
- - a book's info page,
20
- - a specific chapter page,
19
+ * a book's info page,
20
+ * a specific chapter page,
21
21
  and manage login/shutdown asynchronously.
22
22
  """
23
23
 
@@ -15,8 +15,8 @@ from novel_downloader.models import BookInfoDict, ChapterDict
15
15
  class ParserProtocol(Protocol):
16
16
  """
17
17
  A parser must be able to:
18
- - extract book metadata from an HTML string,
19
- - extract a single chapter's text from an HTML string
18
+ * extract book metadata from an HTML string,
19
+ * extract a single chapter's text from an HTML string
20
20
  """
21
21
 
22
22
  def parse_book_info(
@@ -43,6 +43,7 @@ class BaseParser(ParserProtocol, abc.ABC):
43
43
  self._config = config
44
44
  self._book_id: str | None = None
45
45
 
46
+ self._save_font_debug = config.save_font_debug
46
47
  self._decode_font: bool = config.decode_font
47
48
  self._use_truncation = config.use_truncation
48
49
  self._base_cache_dir = Path(config.cache_dir)