novel-downloader 2.0.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 (57) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/download.py +3 -3
  3. novel_downloader/cli/export.py +1 -1
  4. novel_downloader/cli/ui.py +7 -7
  5. novel_downloader/config/adapter.py +191 -154
  6. novel_downloader/core/__init__.py +5 -6
  7. novel_downloader/core/exporters/common/txt.py +9 -9
  8. novel_downloader/core/exporters/linovelib/txt.py +9 -9
  9. novel_downloader/core/fetchers/qidian.py +20 -35
  10. novel_downloader/core/interfaces/fetcher.py +2 -2
  11. novel_downloader/core/interfaces/parser.py +2 -2
  12. novel_downloader/core/parsers/base.py +1 -0
  13. novel_downloader/core/parsers/eightnovel.py +2 -2
  14. novel_downloader/core/parsers/esjzone.py +3 -3
  15. novel_downloader/core/parsers/qidian/main_parser.py +747 -12
  16. novel_downloader/core/parsers/qidian/utils/__init__.py +2 -21
  17. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
  18. novel_downloader/core/parsers/xiguashuwu.py +6 -12
  19. novel_downloader/locales/en.json +3 -3
  20. novel_downloader/locales/zh.json +3 -3
  21. novel_downloader/utils/__init__.py +0 -2
  22. novel_downloader/utils/chapter_storage.py +2 -3
  23. novel_downloader/utils/constants.py +1 -3
  24. novel_downloader/utils/cookies.py +32 -17
  25. novel_downloader/utils/crypto_utils/__init__.py +0 -6
  26. novel_downloader/utils/crypto_utils/rc4.py +40 -50
  27. novel_downloader/utils/epub/__init__.py +2 -3
  28. novel_downloader/utils/epub/builder.py +6 -6
  29. novel_downloader/utils/epub/constants.py +5 -5
  30. novel_downloader/utils/epub/documents.py +7 -7
  31. novel_downloader/utils/epub/models.py +8 -8
  32. novel_downloader/utils/epub/utils.py +10 -10
  33. novel_downloader/utils/file_utils/io.py +48 -73
  34. novel_downloader/utils/file_utils/normalize.py +1 -7
  35. novel_downloader/utils/file_utils/sanitize.py +4 -11
  36. novel_downloader/utils/fontocr/__init__.py +13 -0
  37. novel_downloader/utils/{fontocr.py → fontocr/core.py} +70 -61
  38. novel_downloader/utils/fontocr/loader.py +50 -0
  39. novel_downloader/utils/logger.py +80 -56
  40. novel_downloader/utils/network.py +16 -40
  41. novel_downloader/utils/text_utils/text_cleaner.py +39 -30
  42. novel_downloader/utils/text_utils/truncate_utils.py +3 -14
  43. novel_downloader/utils/time_utils/sleep_utils.py +53 -43
  44. novel_downloader/web/main.py +1 -1
  45. novel_downloader/web/pages/search.py +3 -3
  46. {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/METADATA +2 -1
  47. {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/RECORD +51 -55
  48. novel_downloader/core/parsers/qidian/book_info_parser.py +0 -89
  49. novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -470
  50. novel_downloader/core/parsers/qidian/chapter_normal.py +0 -126
  51. novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
  52. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +0 -143
  53. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -110
  54. {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
  55. {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +0 -0
  56. {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
  57. {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -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)
@@ -195,8 +195,8 @@ class EightnovelParser(BaseParser):
195
195
  def _build_id_title_map(cls, html_str: str) -> dict[str, str]:
196
196
  """
197
197
  Extracts two comma-split lists from html_str:
198
- - A numeric list of IDs (one element longer)
199
- - A list of titles
198
+ * A numeric list of IDs (one element longer)
199
+ * A list of titles
200
200
  """
201
201
  id_list = None
202
202
  title_list = None
@@ -128,9 +128,9 @@ class EsjzoneParser(BaseParser):
128
128
  or tag == "summary"
129
129
  ):
130
130
  # Handle possible volume title markers:
131
- # - <h2>: standard volume header
132
- # - <p class="non">: alternative volume header style
133
- # - <summary>: fallback for stray <summary> tags outside <details>
131
+ # * <h2>: standard volume header
132
+ # * <p class="non">: alternative volume header style
133
+ # * <summary>: fallback for stray <summary> tags outside <details>
134
134
  _start_volume(node.xpath("string()"))
135
135
 
136
136
  elif tag == "a":