novel-downloader 1.5.0__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +1 -3
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +26 -21
  5. novel_downloader/cli/download.py +79 -66
  6. novel_downloader/cli/export.py +17 -21
  7. novel_downloader/cli/main.py +1 -1
  8. novel_downloader/cli/search.py +62 -65
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +8 -5
  11. novel_downloader/config/adapter.py +206 -209
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +5 -5
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +3 -24
  21. novel_downloader/core/downloaders/base.py +49 -23
  22. novel_downloader/core/downloaders/common.py +191 -137
  23. novel_downloader/core/downloaders/qianbi.py +187 -146
  24. novel_downloader/core/downloaders/qidian.py +187 -141
  25. novel_downloader/core/downloaders/registry.py +4 -2
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +3 -20
  28. novel_downloader/core/exporters/base.py +33 -37
  29. novel_downloader/core/exporters/common/__init__.py +1 -2
  30. novel_downloader/core/exporters/common/epub.py +15 -10
  31. novel_downloader/core/exporters/common/main_exporter.py +19 -12
  32. novel_downloader/core/exporters/common/txt.py +17 -12
  33. novel_downloader/core/exporters/epub_util.py +59 -29
  34. novel_downloader/core/exporters/linovelib/__init__.py +1 -0
  35. novel_downloader/core/exporters/linovelib/epub.py +23 -25
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
  37. novel_downloader/core/exporters/linovelib/txt.py +20 -14
  38. novel_downloader/core/exporters/qidian.py +2 -8
  39. novel_downloader/core/exporters/registry.py +4 -2
  40. novel_downloader/core/exporters/txt_util.py +7 -7
  41. novel_downloader/core/fetchers/__init__.py +54 -48
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
  45. novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/lewenn.py +83 -0
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +56 -64
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +5 -16
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/shuhaige.py +84 -0
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/wanbengo.py +83 -0
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +1 -9
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +6 -19
  79. novel_downloader/core/interfaces/parser.py +7 -8
  80. novel_downloader/core/interfaces/searcher.py +9 -1
  81. novel_downloader/core/parsers/__init__.py +49 -12
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +64 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/esjzone.py +64 -69
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/linovelib.py +48 -64
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/qianbi.py +48 -50
  99. novel_downloader/core/parsers/qidian/main_parser.py +756 -48
  100. novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
  101. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  102. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
  103. novel_downloader/core/parsers/quanben5.py +103 -0
  104. novel_downloader/core/parsers/registry.py +5 -16
  105. novel_downloader/core/parsers/sfacg.py +38 -45
  106. novel_downloader/core/parsers/shencou.py +215 -0
  107. novel_downloader/core/parsers/shuhaige.py +111 -0
  108. novel_downloader/core/parsers/tongrenquan.py +116 -0
  109. novel_downloader/core/parsers/ttkan.py +132 -0
  110. novel_downloader/core/parsers/wanbengo.py +191 -0
  111. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  112. novel_downloader/core/parsers/xiguashuwu.py +429 -0
  113. novel_downloader/core/parsers/xs63b.py +161 -0
  114. novel_downloader/core/parsers/xshbook.py +134 -0
  115. novel_downloader/core/parsers/yamibo.py +87 -131
  116. novel_downloader/core/parsers/yibige.py +166 -0
  117. novel_downloader/core/searchers/__init__.py +34 -3
  118. novel_downloader/core/searchers/aaatxt.py +107 -0
  119. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  120. novel_downloader/core/searchers/base.py +112 -36
  121. novel_downloader/core/searchers/dxmwx.py +105 -0
  122. novel_downloader/core/searchers/eightnovel.py +84 -0
  123. novel_downloader/core/searchers/esjzone.py +43 -25
  124. novel_downloader/core/searchers/hetushu.py +92 -0
  125. novel_downloader/core/searchers/i25zw.py +93 -0
  126. novel_downloader/core/searchers/ixdzs8.py +107 -0
  127. novel_downloader/core/searchers/jpxs123.py +107 -0
  128. novel_downloader/core/searchers/piaotia.py +100 -0
  129. novel_downloader/core/searchers/qbtr.py +106 -0
  130. novel_downloader/core/searchers/qianbi.py +74 -40
  131. novel_downloader/core/searchers/quanben5.py +144 -0
  132. novel_downloader/core/searchers/registry.py +24 -8
  133. novel_downloader/core/searchers/shuhaige.py +124 -0
  134. novel_downloader/core/searchers/tongrenquan.py +110 -0
  135. novel_downloader/core/searchers/ttkan.py +92 -0
  136. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  137. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  138. novel_downloader/core/searchers/xs63b.py +104 -0
  139. novel_downloader/locales/en.json +34 -85
  140. novel_downloader/locales/zh.json +35 -86
  141. novel_downloader/models/__init__.py +21 -22
  142. novel_downloader/models/book.py +44 -0
  143. novel_downloader/models/config.py +4 -37
  144. novel_downloader/models/login.py +1 -1
  145. novel_downloader/models/search.py +5 -0
  146. novel_downloader/resources/config/settings.toml +8 -70
  147. novel_downloader/resources/json/xiguashuwu.json +718 -0
  148. novel_downloader/utils/__init__.py +13 -24
  149. novel_downloader/utils/chapter_storage.py +5 -5
  150. novel_downloader/utils/constants.py +4 -31
  151. novel_downloader/utils/cookies.py +38 -35
  152. novel_downloader/utils/crypto_utils/__init__.py +7 -0
  153. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  154. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  155. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  156. novel_downloader/utils/crypto_utils/rc4.py +54 -0
  157. novel_downloader/utils/epub/__init__.py +3 -4
  158. novel_downloader/utils/epub/builder.py +6 -6
  159. novel_downloader/utils/epub/constants.py +62 -21
  160. novel_downloader/utils/epub/documents.py +95 -201
  161. novel_downloader/utils/epub/models.py +8 -22
  162. novel_downloader/utils/epub/utils.py +73 -106
  163. novel_downloader/utils/file_utils/__init__.py +2 -23
  164. novel_downloader/utils/file_utils/io.py +53 -188
  165. novel_downloader/utils/file_utils/normalize.py +1 -7
  166. novel_downloader/utils/file_utils/sanitize.py +4 -15
  167. novel_downloader/utils/fontocr/__init__.py +5 -14
  168. novel_downloader/utils/fontocr/core.py +216 -0
  169. novel_downloader/utils/fontocr/loader.py +50 -0
  170. novel_downloader/utils/logger.py +81 -65
  171. novel_downloader/utils/network.py +17 -41
  172. novel_downloader/utils/state.py +4 -90
  173. novel_downloader/utils/text_utils/__init__.py +1 -7
  174. novel_downloader/utils/text_utils/diff_display.py +5 -7
  175. novel_downloader/utils/text_utils/text_cleaner.py +39 -30
  176. novel_downloader/utils/text_utils/truncate_utils.py +3 -14
  177. novel_downloader/utils/time_utils/__init__.py +5 -11
  178. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  179. novel_downloader/utils/time_utils/sleep_utils.py +55 -49
  180. novel_downloader/web/__init__.py +13 -0
  181. novel_downloader/web/components/__init__.py +11 -0
  182. novel_downloader/web/components/navigation.py +35 -0
  183. novel_downloader/web/main.py +66 -0
  184. novel_downloader/web/pages/__init__.py +17 -0
  185. novel_downloader/web/pages/download.py +78 -0
  186. novel_downloader/web/pages/progress.py +147 -0
  187. novel_downloader/web/pages/search.py +329 -0
  188. novel_downloader/web/services/__init__.py +17 -0
  189. novel_downloader/web/services/client_dialog.py +164 -0
  190. novel_downloader/web/services/cred_broker.py +113 -0
  191. novel_downloader/web/services/cred_models.py +35 -0
  192. novel_downloader/web/services/task_manager.py +264 -0
  193. novel_downloader-2.0.1.dist-info/METADATA +172 -0
  194. novel_downloader-2.0.1.dist-info/RECORD +206 -0
  195. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
  196. novel_downloader/core/downloaders/biquge.py +0 -29
  197. novel_downloader/core/downloaders/esjzone.py +0 -29
  198. novel_downloader/core/downloaders/linovelib.py +0 -29
  199. novel_downloader/core/downloaders/sfacg.py +0 -29
  200. novel_downloader/core/downloaders/yamibo.py +0 -29
  201. novel_downloader/core/exporters/biquge.py +0 -22
  202. novel_downloader/core/exporters/esjzone.py +0 -22
  203. novel_downloader/core/exporters/qianbi.py +0 -22
  204. novel_downloader/core/exporters/sfacg.py +0 -22
  205. novel_downloader/core/exporters/yamibo.py +0 -22
  206. novel_downloader/core/fetchers/base/__init__.py +0 -14
  207. novel_downloader/core/fetchers/base/browser.py +0 -422
  208. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  209. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  210. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  211. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  212. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  213. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  214. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  215. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  216. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  217. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  218. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  219. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  220. novel_downloader/core/parsers/biquge.py +0 -139
  221. novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
  222. novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
  223. novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
  224. novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
  225. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
  226. novel_downloader/models/chapter.py +0 -25
  227. novel_downloader/models/types.py +0 -13
  228. novel_downloader/tui/__init__.py +0 -7
  229. novel_downloader/tui/app.py +0 -32
  230. novel_downloader/tui/main.py +0 -17
  231. novel_downloader/tui/screens/__init__.py +0 -14
  232. novel_downloader/tui/screens/home.py +0 -198
  233. novel_downloader/tui/screens/login.py +0 -74
  234. novel_downloader/tui/styles/home_layout.tcss +0 -79
  235. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  236. novel_downloader/utils/cache.py +0 -24
  237. novel_downloader/utils/crypto_utils.py +0 -71
  238. novel_downloader/utils/fontocr/hash_store.py +0 -280
  239. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  240. novel_downloader/utils/fontocr/model_loader.py +0 -69
  241. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  242. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  243. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  244. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  245. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  246. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
  247. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
  248. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -3,45 +3,34 @@
3
3
  novel_downloader.utils
4
4
  ----------------------
5
5
 
6
+ A collection of helper functions and classes.
6
7
  """
7
8
 
8
9
  __all__ = [
9
10
  "ChapterStorage",
10
11
  "TextCleaner",
11
- "resolve_cookies",
12
- "parse_cookie_expires",
13
- "find_cookie_value",
14
- "rc4_crypt",
12
+ "parse_cookies",
13
+ "get_cookie_value",
15
14
  "sanitize_filename",
16
- "save_as_json",
17
- "save_as_txt",
18
- "read_text_file",
19
- "read_json_file",
20
- "read_binary_file",
15
+ "write_file",
21
16
  "download",
22
17
  "get_cleaner",
23
18
  "content_prefix",
24
19
  "truncate_half_lines",
25
20
  "diff_inline_display",
26
- "calculate_time_difference",
27
- "async_sleep_with_random_delay",
28
- "sleep_with_random_delay",
21
+ "time_diff",
22
+ "async_jitter_sleep",
23
+ "jitter_sleep",
29
24
  ]
30
25
 
31
26
  from .chapter_storage import ChapterStorage
32
27
  from .cookies import (
33
- find_cookie_value,
34
- parse_cookie_expires,
35
- resolve_cookies,
28
+ get_cookie_value,
29
+ parse_cookies,
36
30
  )
37
- from .crypto_utils import rc4_crypt
38
31
  from .file_utils import (
39
- read_binary_file,
40
- read_json_file,
41
- read_text_file,
42
32
  sanitize_filename,
43
- save_as_json,
44
- save_as_txt,
33
+ write_file,
45
34
  )
46
35
  from .network import download
47
36
  from .text_utils import (
@@ -52,7 +41,7 @@ from .text_utils import (
52
41
  truncate_half_lines,
53
42
  )
54
43
  from .time_utils import (
55
- async_sleep_with_random_delay,
56
- calculate_time_difference,
57
- sleep_with_random_delay,
44
+ async_jitter_sleep,
45
+ jitter_sleep,
46
+ time_diff,
58
47
  )
@@ -3,16 +3,17 @@
3
3
  novel_downloader.utils.chapter_storage
4
4
  --------------------------------------
5
5
 
6
- Storage module for managing novel chapters in
7
- either JSON file form or an SQLite database.
6
+ Storage module for managing novel chapters in an SQLite database.
8
7
  """
9
8
 
9
+ __all__ = ["ChapterStorage"]
10
+
10
11
  import contextlib
11
12
  import json
12
13
  import sqlite3
13
14
  import types
14
15
  from pathlib import Path
15
- from typing import Any, Self, cast
16
+ from typing import Any, Self
16
17
 
17
18
  from novel_downloader.models import ChapterDict
18
19
 
@@ -312,8 +313,7 @@ class ChapterStorage:
312
313
  @staticmethod
313
314
  def _load_dict(data: str) -> dict[str, Any]:
314
315
  try:
315
- parsed = json.loads(data)
316
- return cast(dict[str, Any], parsed)
316
+ return json.loads(data) or {}
317
317
  except Exception:
318
318
  return {}
319
319
 
@@ -16,14 +16,12 @@ from platformdirs import user_config_path
16
16
  # -----------------------------------------------------------------------------
17
17
  PACKAGE_NAME = "novel_downloader" # Python package name
18
18
  APP_NAME = "NovelDownloader" # Display name
19
- APP_DIR_NAME = "novel_downloader" # Directory name for platformdirs
20
- LOGGER_NAME = PACKAGE_NAME # Root logger name
21
19
 
22
20
  # -----------------------------------------------------------------------------
23
21
  # Base directories
24
22
  # -----------------------------------------------------------------------------
25
23
  # Base config directory (e.g. ~/AppData/Local/novel_downloader/)
26
- BASE_CONFIG_DIR = Path(user_config_path(APP_DIR_NAME, appauthor=False))
24
+ BASE_CONFIG_DIR = user_config_path(PACKAGE_NAME, appauthor=False)
27
25
  WORK_DIR = Path.cwd()
28
26
  PACKAGE_ROOT: Path = Path(__file__).parent.parent
29
27
  LOCALES_DIR: Path = PACKAGE_ROOT / "locales"
@@ -33,20 +31,17 @@ LOGGER_DIR = WORK_DIR / "logs"
33
31
  JS_SCRIPT_DIR = BASE_CONFIG_DIR / "scripts"
34
32
  DATA_DIR = BASE_CONFIG_DIR / "data"
35
33
  CONFIG_DIR = BASE_CONFIG_DIR / "config"
36
- MODEL_CACHE_DIR = BASE_CONFIG_DIR / "models"
37
34
 
38
35
  # -----------------------------------------------------------------------------
39
36
  # Default file paths
40
37
  # -----------------------------------------------------------------------------
41
38
  STATE_FILE = DATA_DIR / "state.json"
42
39
  SETTING_FILE = CONFIG_DIR / "settings.json"
43
- DEFAULT_USER_DATA_DIR = DATA_DIR / "browser_data"
44
40
 
45
41
 
46
42
  # -----------------------------------------------------------------------------
47
43
  # Default preferences & headers
48
44
  # -----------------------------------------------------------------------------
49
- DEFAULT_USER_PROFILE_NAME = "Profile_1"
50
45
  DEFAULT_IMAGE_SUFFIX = ".jpg"
51
46
 
52
47
  DEFAULT_USER_AGENT = (
@@ -90,33 +85,11 @@ VOLUME_BORDER_IMAGE_PATH = files("novel_downloader.resources.images").joinpath(
90
85
  LINOVELIB_FONT_MAP_PATH = files("novel_downloader.resources.json").joinpath(
91
86
  "linovelib_font_map.json"
92
87
  )
88
+ XIGUASHUWU_FONT_MAP_PATH = files("novel_downloader.resources.json").joinpath(
89
+ "xiguashuwu.json"
90
+ )
93
91
 
94
92
  # JavaScript
95
93
  QD_DECRYPT_SCRIPT_PATH = files("novel_downloader.resources.js_scripts").joinpath(
96
94
  "qidian_decrypt_node.js"
97
95
  )
98
-
99
- # ---------------------------------------------------------------------
100
- # Pretrained model registry (e.g. used in font recovery or OCR)
101
- # ---------------------------------------------------------------------
102
-
103
- # Hugging Face model repo for character recognition
104
- REC_CHAR_MODEL_REPO = "saudadez/rec_chinese_char"
105
-
106
- # Required files to be downloaded for the model
107
- REC_CHAR_MODEL_FILES = [
108
- "inference.pdmodel",
109
- "inference.pdiparams",
110
- "rec_custom_keys.txt",
111
- "char_freq.json",
112
- ]
113
-
114
- REC_CHAR_VECTOR_FILES = [
115
- "char_vectors.npy",
116
- "char_vectors.txt",
117
- ]
118
-
119
- REC_IMAGE_SHAPE_MAP = {
120
- "v1.0": "3,32,32",
121
- "v2.0": "3,48,48",
122
- }
@@ -6,61 +6,64 @@ novel_downloader.utils.cookies
6
6
  Utility for normalizing cookie input from user configuration.
7
7
  """
8
8
 
9
+ __all__ = ["parse_cookies", "get_cookie_value"]
10
+
11
+ import functools
9
12
  import json
10
13
  from collections.abc import Mapping
11
- from email.utils import parsedate_to_datetime
12
- from http.cookies import SimpleCookie
13
14
  from pathlib import Path
15
+ from typing import Any
14
16
 
15
17
 
16
- def resolve_cookies(cookies: str | Mapping[str, str]) -> dict[str, str]:
18
+ def parse_cookies(cookies: str | Mapping[str, str]) -> dict[str, str]:
17
19
  """
18
20
  Parse cookies from a string or dictionary into a standard dictionary.
19
21
 
20
22
  Supports input like:
21
- - "key1=value1; key2=value2"
22
- - {"key1": "value1", "key2": "value2"}
23
+ * `"key1=value1; key2=value2"`
24
+ * `{"key1": "value1", "key2": "value2"}`
23
25
 
24
26
  :param cookies: Cookie string or dict-like object (e.g., from config)
25
27
  :return: A normalized cookie dictionary (key -> value)
26
28
  :raises TypeError: If the input is neither string nor dict-like
27
29
  """
28
30
  if isinstance(cookies, str):
29
- filtered = "; ".join(pair for pair in cookies.split(";") if "=" in pair)
30
- parsed = SimpleCookie()
31
- parsed.load(filtered)
32
- return {k: v.value for k, v in parsed.items()}
31
+ result: dict[str, str] = {}
32
+ for part in cookies.split(";"):
33
+ if "=" not in part:
34
+ continue
35
+ key, value = part.split("=", 1)
36
+ key, value = key.strip(), value.strip()
37
+ if not key:
38
+ continue
39
+ result[key] = value
40
+ return result
33
41
  elif isinstance(cookies, Mapping):
34
42
  return {str(k).strip(): str(v).strip() for k, v in cookies.items()}
35
43
  raise TypeError("Unsupported cookie format: must be str or dict-like")
36
44
 
37
45
 
38
- def parse_cookie_expires(value: str | None) -> int:
39
- if not value:
40
- return -1
41
- try:
42
- return int(value)
43
- except (ValueError, TypeError):
44
- try:
45
- dt = parsedate_to_datetime(value)
46
- return int(dt.timestamp())
47
- except Exception:
48
- return -1
49
-
50
-
51
- def find_cookie_value(state_files: list[Path], key: str) -> str:
46
+ def get_cookie_value(state_files: list[Path], key: str) -> str:
52
47
  for state_file in state_files:
53
- try:
54
- with state_file.open("r", encoding="utf-8") as f:
55
- data = json.load(f)
56
- except Exception:
57
- continue
58
-
48
+ mtime = state_file.stat().st_mtime
49
+ data = load_state_file(state_file, mtime)
59
50
  cookies = data.get("cookies", [])
60
- for cookie in cookies:
61
- if cookie.get("name") != key:
62
- continue
63
- value = cookie.get("value")
64
- if isinstance(value, str):
65
- return value
51
+ value = next(
52
+ (
53
+ c.get("value")
54
+ for c in cookies
55
+ if c.get("name") == key and isinstance(c.get("value"), str)
56
+ ),
57
+ None,
58
+ )
59
+ if isinstance(value, str):
60
+ return value
66
61
  return ""
62
+
63
+
64
+ @functools.cache
65
+ def load_state_file(state_file: Path, mtime: float = 0.0) -> dict[str, Any]:
66
+ try:
67
+ return json.loads(state_file.read_text(encoding="utf-8")) or {}
68
+ except (OSError, json.JSONDecodeError):
69
+ return {}
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.crypto_utils
4
+ -----------------------------------
5
+
6
+ Generic cryptographic utilities
7
+ """
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.crypto_utils.aes_util
4
+ --------------------------------------------
5
+
6
+ AES decrypt functions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __all__ = ["aes_cbc_decrypt"]
12
+
13
+ from typing import Any
14
+
15
+ _BLOCK = 16
16
+ _VALID_KEY_SIZES = (16, 24, 32)
17
+
18
+
19
+ def _as_bytes(name: str, b: Any) -> bytes:
20
+ if isinstance(b, bytes):
21
+ return b
22
+ if isinstance(b, bytearray | memoryview):
23
+ return bytes(b)
24
+ raise TypeError(f"{name} must be bytes-like, got {type(b).__name__}")
25
+
26
+
27
+ def _validate_inputs(key: bytes, iv: bytes, data: bytes) -> None:
28
+ if len(iv) != _BLOCK:
29
+ raise ValueError(f"iv must be {_BLOCK} bytes, got {len(iv)}")
30
+ if len(key) not in _VALID_KEY_SIZES:
31
+ raise ValueError(
32
+ f"key length must be one of {_VALID_KEY_SIZES} bytes, got {len(key)}"
33
+ )
34
+ if len(data) % _BLOCK != 0:
35
+ raise ValueError(
36
+ f"ciphertext length must be a multiple of {_BLOCK} bytes, got {len(data)}"
37
+ )
38
+
39
+
40
+ try:
41
+ from Crypto.Cipher import AES as _PyAES
42
+ from Crypto.Util.Padding import unpad as _py_unpad
43
+
44
+ def aes_cbc_decrypt(
45
+ key: bytes, iv: bytes, data: bytes, block_size: int = _BLOCK
46
+ ) -> bytes:
47
+ """
48
+ AES-CBC decrypt + PKCS#7 unpad (PyCryptodome).
49
+
50
+ :param key: AES key (16/24/32 bytes)
51
+ :param iv: Initialization vector (16 bytes)
52
+ :param data: Ciphertext, length multiple of 16
53
+ :return: Plaintext bytes (unpadded)
54
+ :raises TypeError, ValueError: on invalid inputs
55
+ """
56
+ key_b = _as_bytes("key", key)
57
+ iv_b = _as_bytes("iv", iv)
58
+ data_b = _as_bytes("data", data)
59
+ if not data_b:
60
+ return b""
61
+ _validate_inputs(key_b, iv_b, data_b)
62
+ pt = _PyAES.new(key_b, _PyAES.MODE_CBC, iv_b).decrypt(data_b)
63
+ return _py_unpad(pt, block_size, style="pkcs7") # type: ignore[no-any-return]
64
+
65
+ except ImportError:
66
+ print(
67
+ "[crypto_utils] Falling back to pure-Python AES_CBC.\n"
68
+ "Tip: pip install pycryptodome for ~800x faster speed."
69
+ )
70
+ from novel_downloader.utils.crypto_utils.aes_v2 import AES_CBC
71
+
72
+ def aes_cbc_decrypt(
73
+ key: bytes, iv: bytes, data: bytes, block_size: int = _BLOCK
74
+ ) -> bytes:
75
+ """
76
+ AES-CBC decrypt + PKCS#7 unpad (handled by AES_CBC internally).
77
+
78
+ :param key: AES key (16/24/32 bytes)
79
+ :param iv: Initialization vector (16 bytes)
80
+ :param data: Ciphertext, length multiple of 16
81
+ :return: Plaintext bytes (unpadded)
82
+ :raises TypeError, ValueError: on invalid inputs
83
+ """
84
+ key_b = _as_bytes("key", key)
85
+ iv_b = _as_bytes("iv", iv)
86
+ data_b = _as_bytes("data", data)
87
+ if not data_b:
88
+ return b""
89
+ _validate_inputs(key_b, iv_b, data_b)
90
+ return AES_CBC(key_b, iv_b).decrypt_padded(data_b, block_size)