novel-downloader 1.2.2__py3-none-any.whl → 1.3.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 (128) hide show
  1. novel_downloader/__init__.py +1 -2
  2. novel_downloader/cli/__init__.py +0 -1
  3. novel_downloader/cli/clean.py +2 -10
  4. novel_downloader/cli/download.py +16 -22
  5. novel_downloader/cli/interactive.py +0 -1
  6. novel_downloader/cli/main.py +1 -3
  7. novel_downloader/cli/settings.py +8 -8
  8. novel_downloader/config/__init__.py +0 -1
  9. novel_downloader/config/adapter.py +32 -27
  10. novel_downloader/config/loader.py +116 -108
  11. novel_downloader/config/models.py +35 -29
  12. novel_downloader/config/site_rules.py +2 -4
  13. novel_downloader/core/__init__.py +0 -1
  14. novel_downloader/core/downloaders/__init__.py +4 -4
  15. novel_downloader/core/downloaders/base/__init__.py +14 -0
  16. novel_downloader/core/downloaders/{base_async_downloader.py → base/base_async.py} +49 -53
  17. novel_downloader/core/downloaders/{base_downloader.py → base/base_sync.py} +64 -43
  18. novel_downloader/core/downloaders/biquge/__init__.py +12 -0
  19. novel_downloader/core/downloaders/biquge/biquge_sync.py +25 -0
  20. novel_downloader/core/downloaders/common/__init__.py +14 -0
  21. novel_downloader/core/downloaders/{common_asynb_downloader.py → common/common_async.py} +42 -33
  22. novel_downloader/core/downloaders/{common_downloader.py → common/common_sync.py} +33 -21
  23. novel_downloader/core/downloaders/qidian/__init__.py +10 -0
  24. novel_downloader/core/downloaders/{qidian_downloader.py → qidian/qidian_sync.py} +79 -62
  25. novel_downloader/core/factory/__init__.py +4 -5
  26. novel_downloader/core/factory/{downloader_factory.py → downloader.py} +25 -26
  27. novel_downloader/core/factory/{parser_factory.py → parser.py} +12 -14
  28. novel_downloader/core/factory/{requester_factory.py → requester.py} +29 -16
  29. novel_downloader/core/factory/{saver_factory.py → saver.py} +4 -9
  30. novel_downloader/core/interfaces/__init__.py +8 -9
  31. novel_downloader/core/interfaces/{async_downloader_protocol.py → async_downloader.py} +4 -5
  32. novel_downloader/core/interfaces/{async_requester_protocol.py → async_requester.py} +23 -12
  33. novel_downloader/core/interfaces/{parser_protocol.py → parser.py} +11 -6
  34. novel_downloader/core/interfaces/{saver_protocol.py → saver.py} +2 -3
  35. novel_downloader/core/interfaces/{downloader_protocol.py → sync_downloader.py} +6 -7
  36. novel_downloader/core/interfaces/{requester_protocol.py → sync_requester.py} +31 -17
  37. novel_downloader/core/parsers/__init__.py +5 -4
  38. novel_downloader/core/parsers/{base_parser.py → base.py} +18 -9
  39. novel_downloader/core/parsers/biquge/__init__.py +10 -0
  40. novel_downloader/core/parsers/biquge/main_parser.py +126 -0
  41. novel_downloader/core/parsers/{common_parser → common}/__init__.py +2 -3
  42. novel_downloader/core/parsers/{common_parser → common}/helper.py +13 -13
  43. novel_downloader/core/parsers/{common_parser → common}/main_parser.py +15 -9
  44. novel_downloader/core/parsers/{qidian_parser → qidian}/__init__.py +2 -3
  45. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/__init__.py +2 -3
  46. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_encrypted.py +40 -48
  47. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_normal.py +17 -21
  48. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_router.py +10 -9
  49. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/main_parser.py +14 -10
  50. novel_downloader/core/parsers/{qidian_parser → qidian}/session/__init__.py +2 -3
  51. novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_encrypted.py +36 -44
  52. novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_normal.py +19 -23
  53. novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_router.py +10 -9
  54. novel_downloader/core/parsers/{qidian_parser → qidian}/session/main_parser.py +14 -10
  55. novel_downloader/core/parsers/{qidian_parser → qidian}/session/node_decryptor.py +7 -10
  56. novel_downloader/core/parsers/{qidian_parser → qidian}/shared/__init__.py +2 -3
  57. novel_downloader/core/parsers/{qidian_parser → qidian}/shared/book_info_parser.py +5 -6
  58. novel_downloader/core/parsers/{qidian_parser → qidian}/shared/helpers.py +7 -8
  59. novel_downloader/core/requesters/__init__.py +9 -5
  60. novel_downloader/core/requesters/base/__init__.py +16 -0
  61. novel_downloader/core/requesters/{base_async_session.py → base/async_session.py} +177 -73
  62. novel_downloader/core/requesters/base/browser.py +340 -0
  63. novel_downloader/core/requesters/base/session.py +364 -0
  64. novel_downloader/core/requesters/biquge/__init__.py +12 -0
  65. novel_downloader/core/requesters/biquge/session.py +90 -0
  66. novel_downloader/core/requesters/{common_requester → common}/__init__.py +4 -5
  67. novel_downloader/core/requesters/common/async_session.py +96 -0
  68. novel_downloader/core/requesters/common/session.py +113 -0
  69. novel_downloader/core/requesters/qidian/__init__.py +21 -0
  70. novel_downloader/core/requesters/qidian/broswer.py +307 -0
  71. novel_downloader/core/requesters/qidian/session.py +287 -0
  72. novel_downloader/core/savers/__init__.py +5 -3
  73. novel_downloader/core/savers/{base_saver.py → base.py} +12 -13
  74. novel_downloader/core/savers/biquge.py +25 -0
  75. novel_downloader/core/savers/{common_saver → common}/__init__.py +2 -3
  76. novel_downloader/core/savers/{common_saver/common_epub.py → common/epub.py} +23 -51
  77. novel_downloader/core/savers/{common_saver → common}/main_saver.py +43 -9
  78. novel_downloader/core/savers/{common_saver/common_txt.py → common/txt.py} +16 -46
  79. novel_downloader/core/savers/epub_utils/__init__.py +0 -1
  80. novel_downloader/core/savers/epub_utils/css_builder.py +13 -7
  81. novel_downloader/core/savers/epub_utils/initializer.py +4 -5
  82. novel_downloader/core/savers/epub_utils/text_to_html.py +2 -3
  83. novel_downloader/core/savers/epub_utils/volume_intro.py +1 -3
  84. novel_downloader/core/savers/{qidian_saver.py → qidian.py} +12 -6
  85. novel_downloader/locales/en.json +8 -4
  86. novel_downloader/locales/zh.json +5 -1
  87. novel_downloader/resources/config/settings.toml +88 -0
  88. novel_downloader/utils/cache.py +2 -2
  89. novel_downloader/utils/chapter_storage.py +340 -0
  90. novel_downloader/utils/constants.py +6 -4
  91. novel_downloader/utils/crypto_utils.py +3 -3
  92. novel_downloader/utils/file_utils/__init__.py +0 -1
  93. novel_downloader/utils/file_utils/io.py +12 -17
  94. novel_downloader/utils/file_utils/normalize.py +1 -3
  95. novel_downloader/utils/file_utils/sanitize.py +2 -9
  96. novel_downloader/utils/fontocr/__init__.py +0 -1
  97. novel_downloader/utils/fontocr/ocr_v1.py +19 -22
  98. novel_downloader/utils/fontocr/ocr_v2.py +147 -60
  99. novel_downloader/utils/hash_store.py +19 -20
  100. novel_downloader/utils/hash_utils.py +0 -1
  101. novel_downloader/utils/i18n.py +3 -4
  102. novel_downloader/utils/logger.py +5 -6
  103. novel_downloader/utils/model_loader.py +5 -8
  104. novel_downloader/utils/network.py +9 -10
  105. novel_downloader/utils/state.py +6 -7
  106. novel_downloader/utils/text_utils/__init__.py +0 -1
  107. novel_downloader/utils/text_utils/chapter_formatting.py +2 -7
  108. novel_downloader/utils/text_utils/diff_display.py +0 -1
  109. novel_downloader/utils/text_utils/font_mapping.py +1 -4
  110. novel_downloader/utils/text_utils/text_cleaning.py +0 -1
  111. novel_downloader/utils/time_utils/__init__.py +0 -1
  112. novel_downloader/utils/time_utils/datetime_utils.py +8 -10
  113. novel_downloader/utils/time_utils/sleep_utils.py +1 -3
  114. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/METADATA +14 -17
  115. novel_downloader-1.3.1.dist-info/RECORD +127 -0
  116. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/WHEEL +1 -1
  117. novel_downloader/core/requesters/base_browser.py +0 -214
  118. novel_downloader/core/requesters/base_session.py +0 -246
  119. novel_downloader/core/requesters/common_requester/common_async_session.py +0 -98
  120. novel_downloader/core/requesters/common_requester/common_session.py +0 -126
  121. novel_downloader/core/requesters/qidian_requester/__init__.py +0 -22
  122. novel_downloader/core/requesters/qidian_requester/qidian_broswer.py +0 -396
  123. novel_downloader/core/requesters/qidian_requester/qidian_session.py +0 -202
  124. novel_downloader/resources/config/settings.yaml +0 -76
  125. novel_downloader-1.2.2.dist-info/RECORD +0 -115
  126. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/entry_points.txt +0 -0
  127. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/licenses/LICENSE +0 -0
  128. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.hash_store
5
4
  ---------------------------------
@@ -11,8 +10,8 @@ Supports loading/saving to .json or .npy, and basic CRUD + search.
11
10
  import heapq
12
11
  import json
13
12
  import logging
13
+ from collections.abc import Callable
14
14
  from pathlib import Path
15
- from typing import Callable, Dict, List, Optional, Set, Tuple, Union
16
15
 
17
16
  import numpy as np
18
17
  from PIL import Image
@@ -33,7 +32,7 @@ class _BKNode:
33
32
 
34
33
  def __init__(self, value: int):
35
34
  self.value = value
36
- self.children: Dict[int, _BKNode] = {}
35
+ self.children: dict[int, _BKNode] = {}
37
36
 
38
37
  def add(self, h: int, dist_fn: Callable[[int, int], int]) -> None:
39
38
  d = dist_fn(h, self.value)
@@ -48,12 +47,12 @@ class _BKNode:
48
47
  target: int,
49
48
  threshold: int,
50
49
  dist_fn: Callable[[int, int], int],
51
- ) -> List[Tuple[int, int]]:
50
+ ) -> list[tuple[int, int]]:
52
51
  """
53
52
  Recursively collect (value, dist) pairs within threshold.
54
53
  """
55
54
  d0 = dist_fn(target, self.value)
56
- matches: List[Tuple[int, int]] = []
55
+ matches: list[tuple[int, int]] = []
57
56
  if d0 <= threshold:
58
57
  matches.append((self.value, d0))
59
58
  # Only children whose edge-dist \in [d0-threshold, d0+threshold]
@@ -76,7 +75,7 @@ class ImageHashStore:
76
75
 
77
76
  def __init__(
78
77
  self,
79
- path: Union[str, Path] = HASH_STORE_FILE,
78
+ path: str | Path = HASH_STORE_FILE,
80
79
  auto_save: bool = False,
81
80
  hash_func: Callable[[Image.Image], int] = phash,
82
81
  ham_dist: Callable[[int, int], int] = fast_hamming_distance,
@@ -89,11 +88,11 @@ class ImageHashStore:
89
88
  self._th = threshold
90
89
 
91
90
  # label -> set of hashes
92
- self._hash: Dict[str, Set[int]] = {}
91
+ self._hash: dict[str, set[int]] = {}
93
92
  # hash -> list of labels (for reverse lookup)
94
- self._hash_to_labels: Dict[int, List[str]] = {}
93
+ self._hash_to_labels: dict[int, list[str]] = {}
95
94
  # root of BK-Tree (or None if empty)
96
- self._bk_root: Optional[_BKNode] = None
95
+ self._bk_root: _BKNode | None = None
97
96
 
98
97
  self.load()
99
98
 
@@ -155,7 +154,7 @@ class ImageHashStore:
155
154
  if self._auto:
156
155
  self.save()
157
156
 
158
- def add_image(self, img_path: Union[str, Path], label: str) -> int:
157
+ def add_image(self, img_path: str | Path, label: str) -> int:
159
158
  """
160
159
  Compute hash for the given image and add it under `label`.
161
160
  Updates BK-Tree index incrementally.
@@ -173,7 +172,7 @@ class ImageHashStore:
173
172
  self._maybe_save()
174
173
  return h
175
174
 
176
- def add_from_map(self, map_path: Union[str, Path]) -> None:
175
+ def add_from_map(self, map_path: str | Path) -> None:
177
176
  """
178
177
  Load a JSON file of the form { "image_path": "label", ... }
179
178
  and add each entry.
@@ -191,11 +190,11 @@ class ImageHashStore:
191
190
  )
192
191
  continue
193
192
 
194
- def labels(self) -> List[str]:
193
+ def labels(self) -> list[str]:
195
194
  """Return a sorted list of all labels in the store."""
196
195
  return sorted(self._hash.keys())
197
196
 
198
- def hashes(self, label: str) -> Set[int]:
197
+ def hashes(self, label: str) -> set[int]:
199
198
  """Return the set of hashes for a given `label` (empty set if none)."""
200
199
  return set(self._hash.get(label, ()))
201
200
 
@@ -206,7 +205,7 @@ class ImageHashStore:
206
205
  logger.debug("[ImageHashStore] Removed label '%s'", label)
207
206
  self._maybe_save()
208
207
 
209
- def remove_hash(self, label: str, this: Union[int, str, Path]) -> bool:
208
+ def remove_hash(self, label: str, this: int | str | Path) -> bool:
210
209
  """
211
210
  Remove a specific hash under `label`.
212
211
  `this` can be:
@@ -218,7 +217,7 @@ class ImageHashStore:
218
217
  return False
219
218
 
220
219
  h = None
221
- if isinstance(this, (str, Path)):
220
+ if isinstance(this, (str | Path)):
222
221
  try:
223
222
  img = Image.open(this).convert("L")
224
223
  h = self._hf(img)
@@ -239,10 +238,10 @@ class ImageHashStore:
239
238
 
240
239
  def query(
241
240
  self,
242
- target: Union[int, str, Path, Image.Image],
241
+ target: int | str | Path | Image.Image,
243
242
  k: int = 1,
244
- threshold: Optional[int] = None,
245
- ) -> List[Tuple[str, float]]:
243
+ threshold: int | None = None,
244
+ ) -> list[tuple[str, float]]:
246
245
  """
247
246
  Find up to `k` distinct labels whose stored hashes are most similar
248
247
  to `target` within `threshold`. Returns a list of (label, score),
@@ -259,7 +258,7 @@ class ImageHashStore:
259
258
  if isinstance(target, Image.Image):
260
259
  img = target.convert("L")
261
260
  thash = self._hf(img)
262
- elif isinstance(target, (str, Path)):
261
+ elif isinstance(target, (str | Path)):
263
262
  img = Image.open(target).convert("L")
264
263
  thash = self._hf(img)
265
264
  else:
@@ -272,7 +271,7 @@ class ImageHashStore:
272
271
  matches = self._bk_root.query(thash, threshold, self._hd)
273
272
 
274
273
  # collapse to one best dist per label
275
- best_per_label: Dict[str, float] = {}
274
+ best_per_label: dict[str, float] = {}
276
275
  h2l = self._hash_to_labels
277
276
  for h, dist in matches:
278
277
  for lbl in h2l.get(h, ()):
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.hash_utils
5
4
  ---------------------------------
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.i18n
5
4
  ---------------------------
@@ -8,17 +7,17 @@ Multilingual text dictionary and utility for CLI and interactive mode.
8
7
  """
9
8
 
10
9
  import json
11
- from typing import Any, Dict
10
+ from typing import Any
12
11
 
13
12
  from novel_downloader.utils.constants import LOCALES_DIR
14
13
  from novel_downloader.utils.state import state_mgr
15
14
 
16
- _TRANSLATIONS: Dict[str, Dict[str, str]] = {}
15
+ _TRANSLATIONS: dict[str, dict[str, str]] = {}
17
16
 
18
17
  for locale_path in LOCALES_DIR.glob("*.json"):
19
18
  lang = locale_path.stem
20
19
  try:
21
- with open(locale_path, "r", encoding="utf-8") as f:
20
+ with open(locale_path, encoding="utf-8") as f:
22
21
  _TRANSLATIONS[lang] = json.load(f)
23
22
  except Exception:
24
23
  continue
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.logger
5
4
  -----------------------------
@@ -12,13 +11,13 @@ import logging
12
11
  from datetime import datetime
13
12
  from logging.handlers import TimedRotatingFileHandler
14
13
  from pathlib import Path
15
- from typing import Dict, Literal, Optional, Union
14
+ from typing import Literal
16
15
 
17
16
  from .constants import LOGGER_DIR, LOGGER_NAME
18
17
 
19
18
  LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"]
20
19
 
21
- LOG_LEVELS: Dict[LogLevel, int] = {
20
+ LOG_LEVELS: dict[LogLevel, int] = {
22
21
  "DEBUG": logging.DEBUG,
23
22
  "INFO": logging.INFO,
24
23
  "WARNING": logging.WARNING,
@@ -27,9 +26,9 @@ LOG_LEVELS: Dict[LogLevel, int] = {
27
26
 
28
27
 
29
28
  def setup_logging(
30
- log_filename_prefix: Optional[str] = None,
31
- log_level: Optional[LogLevel] = None,
32
- log_dir: Optional[Union[str, Path]] = None,
29
+ log_filename_prefix: str | None = None,
30
+ log_level: LogLevel | None = None,
31
+ log_dir: str | Path | None = None,
33
32
  ) -> logging.Logger:
34
33
  """
35
34
  Create and configure a logger for both console and rotating file output.
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.model_loader
5
4
  -----------------------------------
@@ -13,7 +12,7 @@ Currently supports:
13
12
  from pathlib import Path
14
13
 
15
14
  from huggingface_hub import hf_hub_download
16
- from huggingface_hub.utils import LocalEntryNotFoundError
15
+ from huggingface_hub.errors import LocalEntryNotFoundError
17
16
 
18
17
  from novel_downloader.utils.constants import (
19
18
  MODEL_CACHE_DIR,
@@ -38,12 +37,11 @@ def get_rec_chinese_char_model_dir(version: str = "v1.0") -> Path:
38
37
  filename=fname,
39
38
  revision=version,
40
39
  local_dir=model_dir,
41
- local_dir_use_symlinks=False,
42
40
  )
43
- except LocalEntryNotFoundError:
41
+ except LocalEntryNotFoundError as err:
44
42
  raise RuntimeError(
45
43
  f"[model] Missing model file '{fname}' and no internet connection."
46
- )
44
+ ) from err
47
45
  return model_dir
48
46
 
49
47
 
@@ -62,11 +60,10 @@ def get_rec_char_vector_dir(version: str = "v1.0") -> Path:
62
60
  filename=fname,
63
61
  revision=version,
64
62
  local_dir=vector_dir,
65
- local_dir_use_symlinks=False,
66
63
  )
67
- except LocalEntryNotFoundError:
64
+ except LocalEntryNotFoundError as err:
68
65
  raise RuntimeError(
69
66
  f"[vector] Missing vector file '{fname}' and no internet connection."
70
- )
67
+ ) from err
71
68
 
72
69
  return vector_dir
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.network
5
4
  ------------------------------
@@ -11,7 +10,7 @@ import logging
11
10
  import random
12
11
  import time
13
12
  from pathlib import Path
14
- from typing import Dict, Literal, Optional, Union
13
+ from typing import Literal
15
14
  from urllib.parse import unquote, urlparse
16
15
 
17
16
  import requests
@@ -30,9 +29,9 @@ def http_get_with_retry(
30
29
  retries: int = 3,
31
30
  timeout: int = 10,
32
31
  backoff: float = 0.5,
33
- headers: Optional[Dict[str, str]] = None,
32
+ headers: dict[str, str] | None = None,
34
33
  stream: bool = False,
35
- ) -> Optional[requests.Response]:
34
+ ) -> requests.Response | None:
36
35
  """
37
36
  Perform a GET request with retry support.
38
37
 
@@ -87,13 +86,13 @@ def image_url_to_filename(url: str) -> str:
87
86
 
88
87
  def download_image_as_bytes(
89
88
  url: str,
90
- target_folder: Optional[Union[str, Path]] = None,
89
+ target_folder: str | Path | None = None,
91
90
  *,
92
91
  timeout: int = 10,
93
92
  retries: int = 3,
94
93
  backoff: float = 0.5,
95
94
  on_exist: Literal["overwrite", "skip", "rename"] = "overwrite",
96
- ) -> Optional[bytes]:
95
+ ) -> bytes | None:
97
96
  """
98
97
  Download an image from a given URL and return its content as bytes.
99
98
 
@@ -155,13 +154,13 @@ def download_image_as_bytes(
155
154
 
156
155
  def download_font_file(
157
156
  url: str,
158
- target_folder: Union[str, Path],
157
+ target_folder: str | Path,
159
158
  *,
160
159
  timeout: int = 10,
161
160
  retries: int = 3,
162
161
  backoff: float = 0.5,
163
162
  on_exist: Literal["overwrite", "skip", "rename"] = "skip",
164
- ) -> Optional[Path]:
163
+ ) -> Path | None:
165
164
  """
166
165
  Download a font file from a URL and save it locally with retry and overwrite control
167
166
 
@@ -226,13 +225,13 @@ def download_font_file(
226
225
 
227
226
  def download_js_file(
228
227
  url: str,
229
- target_folder: Union[str, Path],
228
+ target_folder: str | Path,
230
229
  *,
231
230
  timeout: int = 10,
232
231
  retries: int = 3,
233
232
  backoff: float = 0.5,
234
233
  on_exist: Literal["overwrite", "skip", "rename"] = "skip",
235
- ) -> Optional[Path]:
234
+ ) -> Path | None:
236
235
  """
237
236
  Download a JavaScript (.js) file from a URL and save it locally.
238
237
 
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.state
5
4
  ----------------------------
@@ -11,7 +10,7 @@ Supported sections:
11
10
  """
12
11
  import json
13
12
  from pathlib import Path
14
- from typing import Any, Dict, Union
13
+ from typing import Any
15
14
 
16
15
  from .constants import STATE_FILE
17
16
 
@@ -26,7 +25,7 @@ class StateManager:
26
25
  self._path = path
27
26
  self._data = self._load()
28
27
 
29
- def _load(self) -> Dict[str, Any]:
28
+ def _load(self) -> dict[str, Any]:
30
29
  """
31
30
  Load the configuration file into a Python dictionary.
32
31
 
@@ -50,7 +49,7 @@ class StateManager:
50
49
  content = json.dumps(self._data, ensure_ascii=False, indent=2)
51
50
  self._path.write_text(content, encoding="utf-8")
52
51
 
53
- def _parse_cookie_string(self, cookie_str: str) -> Dict[str, str]:
52
+ def _parse_cookie_string(self, cookie_str: str) -> dict[str, str]:
54
53
  """
55
54
  Parse a Cookie header string into a dict.
56
55
 
@@ -58,7 +57,7 @@ class StateManager:
58
57
  :return: mapping cookie names to values (missing '=' yields empty string)
59
58
  :rtype: Dict[str, str]
60
59
  """
61
- cookies: Dict[str, str] = {}
60
+ cookies: dict[str, str] = {}
62
61
  for item in cookie_str.split(";"):
63
62
  item = item.strip()
64
63
  if not item:
@@ -110,7 +109,7 @@ class StateManager:
110
109
  site_data["manual_login"] = flag
111
110
  self._save()
112
111
 
113
- def get_cookies(self, site: str) -> Dict[str, str]:
112
+ def get_cookies(self, site: str) -> dict[str, str]:
114
113
  """
115
114
  Retrieve the persisted cookies for a specific site.
116
115
 
@@ -120,7 +119,7 @@ class StateManager:
120
119
  cookies = self._data.get("sites", {}).get(site, {}).get("cookies", {})
121
120
  return {str(k): str(v) for k, v in cookies.items()}
122
121
 
123
- def set_cookies(self, site: str, cookies: Union[str, Dict[str, str]]) -> None:
122
+ def set_cookies(self, site: str, cookies: str | dict[str, str]) -> None:
124
123
  """
125
124
  Persist (overwrite) the cookies for a specific site.
126
125
 
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.text_utils
5
4
  ---------------------------------
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.text_utils.chapter_formatting
5
4
  ----------------------------------------------------
@@ -7,12 +6,8 @@ novel_downloader.utils.text_utils.chapter_formatting
7
6
  Format chapter content with title, paragraph blocks, and optional author notes.
8
7
  """
9
8
 
10
- from typing import List, Optional
11
9
 
12
-
13
- def format_chapter(
14
- title: str, paragraphs: str, author_say: Optional[str] = None
15
- ) -> str:
10
+ def format_chapter(title: str, paragraphs: str, author_say: str | None = None) -> str:
16
11
  """
17
12
  Build a formatted chapter string with title, paragraphs, and optional author note.
18
13
 
@@ -22,7 +17,7 @@ def format_chapter(
22
17
  :return: A single string where title, paragraphs, and author note
23
18
  are separated by blank lines.
24
19
  """
25
- parts: List[str] = [title.strip()]
20
+ parts: list[str] = [title.strip()]
26
21
 
27
22
  # add each nonempty paragraph line
28
23
  for ln in paragraphs.splitlines():
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.text_utils.diff_display
5
4
  ----------------------------------------------
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.text_utils.font_mapping
5
4
  ----------------------------------------------
@@ -11,10 +10,8 @@ where characters are visually disguised via custom font glyphs but can be
11
10
  recovered using a known mapping.
12
11
  """
13
12
 
14
- from typing import Dict
15
13
 
16
-
17
- def apply_font_mapping(text: str, font_map: Dict[str, str]) -> str:
14
+ def apply_font_mapping(text: str, font_map: dict[str, str]) -> str:
18
15
  """
19
16
  Replace each character in `text` using `font_map`,
20
17
  leaving unmapped characters unchanged.
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.text_utils.text_cleaning
5
4
  -----------------------------------------------
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.time_utils
5
4
  ---------------------------------
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.time_utils.datetime_utils
5
4
  ------------------------------------------------
@@ -15,8 +14,7 @@ Includes:
15
14
 
16
15
  import logging
17
16
  import re
18
- from datetime import datetime, timedelta, timezone
19
- from typing import Optional, Tuple
17
+ from datetime import UTC, datetime, timedelta, timezone
20
18
 
21
19
  logger = logging.getLogger(__name__)
22
20
 
@@ -61,8 +59,8 @@ def _parse_utc_offset(tz_str: str) -> timezone:
61
59
  else:
62
60
  try:
63
61
  hours = int(offset_part)
64
- except ValueError:
65
- raise ValueError(f"Invalid UTC offset hours: '{offset_part}'")
62
+ except ValueError as err:
63
+ raise ValueError(f"Invalid UTC offset hours: '{offset_part}'") from err
66
64
  return timezone(timedelta(hours=hours))
67
65
 
68
66
 
@@ -100,9 +98,9 @@ def _parse_datetime_flexible(dt_str: str) -> datetime:
100
98
  def calculate_time_difference(
101
99
  from_time_str: str,
102
100
  tz_str: str = "UTC",
103
- to_time_str: Optional[str] = None,
101
+ to_time_str: str | None = None,
104
102
  to_tz_str: str = "UTC",
105
- ) -> Tuple[int, int, int, int]:
103
+ ) -> tuple[int, int, int, int]:
106
104
  """
107
105
  Calculate the difference between two datetime values.
108
106
 
@@ -116,15 +114,15 @@ def calculate_time_difference(
116
114
  # parse start time
117
115
  tz_from = _parse_utc_offset(tz_str)
118
116
  dt_from = _parse_datetime_flexible(from_time_str)
119
- dt_from = dt_from.replace(tzinfo=tz_from).astimezone(timezone.utc)
117
+ dt_from = dt_from.replace(tzinfo=tz_from).astimezone(UTC)
120
118
 
121
119
  # parse end time or use now
122
120
  if to_time_str:
123
121
  tz_to = _parse_utc_offset(to_tz_str)
124
122
  dt_to = _parse_datetime_flexible(to_time_str)
125
- dt_to = dt_to.replace(tzinfo=tz_to).astimezone(timezone.utc)
123
+ dt_to = dt_to.replace(tzinfo=tz_to).astimezone(UTC)
126
124
  else:
127
- dt_to = datetime.now(timezone.utc)
125
+ dt_to = datetime.now(UTC)
128
126
 
129
127
  delta = dt_to - dt_from
130
128
 
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.utils.time_utils.sleep_utils
5
4
  ---------------------------------------------
@@ -14,7 +13,6 @@ Includes:
14
13
  import logging
15
14
  import random
16
15
  import time
17
- from typing import Optional
18
16
 
19
17
  logger = logging.getLogger(__name__)
20
18
 
@@ -24,7 +22,7 @@ def sleep_with_random_delay(
24
22
  add_spread: float = 0.0,
25
23
  mul_spread: float = 1.0,
26
24
  *,
27
- max_sleep: Optional[float] = None,
25
+ max_sleep: float | None = None,
28
26
  ) -> None:
29
27
  """
30
28
  Sleep for a random duration by combining multiplicative and additive jitter.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: novel-downloader
3
- Version: 1.2.2
3
+ Version: 1.3.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
@@ -34,18 +34,16 @@ Classifier: License :: OSI Approved :: MIT License
34
34
  Classifier: Natural Language :: Chinese (Simplified)
35
35
  Classifier: Topic :: Utilities
36
36
  Classifier: Programming Language :: Python :: 3
37
- Classifier: Programming Language :: Python :: 3.8
38
- Classifier: Programming Language :: Python :: 3.9
39
- Classifier: Programming Language :: Python :: 3.10
40
- Classifier: Programming Language :: Python :: 3.11
41
37
  Classifier: Programming Language :: Python :: 3.12
42
- Requires-Python: >=3.8
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Requires-Python: >=3.12
43
40
  Description-Content-Type: text/markdown
44
41
  License-File: LICENSE
45
42
  Requires-Dist: requests
43
+ Requires-Dist: aiohttp
46
44
  Requires-Dist: beautifulsoup4
47
45
  Requires-Dist: DrissionPage
48
- Requires-Dist: pyyaml
46
+ Requires-Dist: opencv-python
49
47
  Requires-Dist: lxml
50
48
  Requires-Dist: platformdirs
51
49
  Requires-Dist: click
@@ -64,10 +62,9 @@ Requires-Dist: scipy; extra == "font-recovery"
64
62
  Requires-Dist: numpy; extra == "font-recovery"
65
63
  Requires-Dist: tinycss2; extra == "font-recovery"
66
64
  Requires-Dist: fonttools; extra == "font-recovery"
65
+ Requires-Dist: brotli; extra == "font-recovery"
67
66
  Requires-Dist: pillow; extra == "font-recovery"
68
67
  Requires-Dist: huggingface_hub; extra == "font-recovery"
69
- Provides-Extra: async
70
- Requires-Dist: aiohttp; extra == "async"
71
68
  Dynamic: license-file
72
69
 
73
70
  # novel-downloader
@@ -94,13 +91,10 @@ pip install novel-downloader
94
91
  # 如需支持字体解密功能 (decode_font), 请使用:
95
92
  # pip install novel-downloader[font-recovery]
96
93
 
97
- # 如需启用异步抓取模式 (mode=async), 请使用:
98
- # pip install novel-downloader[async]
99
-
100
- # 初始化默认配置 (生成 settings.yaml)
94
+ # 初始化默认配置 (生成 settings.toml)
101
95
  novel-cli settings init
102
96
 
103
- # 编辑 ./settings.yaml 完成 site/book_ids 等
97
+ # 编辑 ./settings.toml 完成 site/book_ids 等
104
98
  # 可查看 docs/4-settings-schema.md
105
99
 
106
100
  # 运行下载
@@ -117,7 +111,6 @@ cd novel-downloader
117
111
  pip install .
118
112
  # 或安装带可选功能:
119
113
  # pip install .[font-recovery]
120
- # pip install .[async]
121
114
  ```
122
115
 
123
116
  更多使用方法, 查看 [使用示例](https://github.com/BowenZ217/novel-downloader/blob/main/docs/5-usage-examples.md)
@@ -127,7 +120,10 @@ pip install .
127
120
  ## 功能特性
128
121
 
129
122
  - 爬取起点中文网的小说章节内容 (支持免费与已订阅章节)
130
- - 自动整合所有章节并输出为完整的 TXT 文件
123
+ - 断点续爬
124
+ - 自动整合所有章节并导出为
125
+ - TXT
126
+ - EPUB
131
127
  - 支持活动广告过滤:
132
128
  - [x] 章节标题
133
129
  - [ ] 章节正文
@@ -141,8 +137,9 @@ pip install .
141
137
  - [安装](https://github.com/BowenZ217/novel-downloader/blob/main/docs/1-installation.md)
142
138
  - [环境准备](https://github.com/BowenZ217/novel-downloader/blob/main/docs/2-environment-setup.md)
143
139
  - [配置](https://github.com/BowenZ217/novel-downloader/blob/main/docs/3-configuration.md)
144
- - [settings.yaml 配置说明](https://github.com/BowenZ217/novel-downloader/blob/main/docs/4-settings-schema.md)
140
+ - [settings.toml 配置说明](https://github.com/BowenZ217/novel-downloader/blob/main/docs/4-settings-schema.md)
145
141
  - [使用示例](https://github.com/BowenZ217/novel-downloader/blob/main/docs/5-usage-examples.md)
142
+ - [支持站点列表](https://github.com/BowenZ217/novel-downloader/blob/main/docs/6-supported-sites.md)
146
143
  - [文件保存](https://github.com/BowenZ217/novel-downloader/blob/main/docs/file-saving.md)
147
144
  - [TODO](https://github.com/BowenZ217/novel-downloader/blob/main/docs/todo.md)
148
145
  - [开发](https://github.com/BowenZ217/novel-downloader/blob/main/docs/develop.md)