novel-downloader 1.4.2__py3-none-any.whl → 1.4.4__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 (41) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/download.py +1 -1
  3. novel_downloader/config/adapter.py +1 -0
  4. novel_downloader/core/__init__.py +19 -1
  5. novel_downloader/core/downloaders/base.py +0 -7
  6. novel_downloader/core/downloaders/biquge.py +1 -3
  7. novel_downloader/core/downloaders/common.py +0 -2
  8. novel_downloader/core/downloaders/esjzone.py +1 -3
  9. novel_downloader/core/downloaders/linovelib.py +1 -3
  10. novel_downloader/core/downloaders/qianbi.py +1 -3
  11. novel_downloader/core/downloaders/qidian.py +1 -5
  12. novel_downloader/core/downloaders/sfacg.py +1 -3
  13. novel_downloader/core/downloaders/yamibo.py +1 -3
  14. novel_downloader/core/factory/downloader.py +3 -6
  15. novel_downloader/core/fetchers/base/browser.py +32 -12
  16. novel_downloader/core/fetchers/esjzone/browser.py +8 -6
  17. novel_downloader/core/fetchers/qidian/browser.py +2 -2
  18. novel_downloader/core/fetchers/qidian/session.py +2 -1
  19. novel_downloader/core/fetchers/yamibo/browser.py +3 -3
  20. novel_downloader/core/parsers/qidian/book_info_parser.py +31 -54
  21. novel_downloader/core/parsers/qidian/chapter_encrypted.py +11 -2
  22. novel_downloader/core/parsers/qidian/chapter_normal.py +8 -1
  23. novel_downloader/core/parsers/qidian/main_parser.py +7 -2
  24. novel_downloader/core/parsers/qidian/utils/__init__.py +2 -0
  25. novel_downloader/core/parsers/qidian/utils/helpers.py +9 -0
  26. novel_downloader/models/config.py +1 -0
  27. novel_downloader/resources/config/settings.toml +1 -0
  28. novel_downloader/tui/screens/home.py +8 -2
  29. novel_downloader/tui/screens/login.py +1 -1
  30. novel_downloader/utils/{model_loader.py → fontocr/model_loader.py} +2 -2
  31. novel_downloader/utils/fontocr/ocr_v1.py +2 -1
  32. novel_downloader/utils/fontocr/ocr_v2.py +2 -1
  33. novel_downloader/utils/text_utils/__init__.py +8 -1
  34. novel_downloader/utils/text_utils/text_cleaning.py +51 -0
  35. novel_downloader/utils/time_utils/datetime_utils.py +1 -1
  36. {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/METADATA +2 -1
  37. {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/RECORD +41 -41
  38. {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/WHEEL +0 -0
  39. {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/entry_points.txt +0 -0
  40. {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/licenses/LICENSE +0 -0
  41. {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ novel_downloader
6
6
  Core package for the Novel Downloader project.
7
7
  """
8
8
 
9
- __version__ = "1.4.2"
9
+ __version__ = "1.4.4"
10
10
 
11
11
  __author__ = "Saudade Z"
12
12
  __email__ = "saudadez217@gmail.com"
@@ -161,7 +161,6 @@ async def _download(
161
161
  downloader = get_downloader(
162
162
  fetcher=fetcher,
163
163
  parser=parser,
164
- exporter=exporter,
165
164
  site=site,
166
165
  config=downloader_cfg,
167
166
  )
@@ -172,6 +171,7 @@ async def _download(
172
171
  book,
173
172
  progress_hook=_print_progress,
174
173
  )
174
+ await asyncio.to_thread(exporter.export, book["book_id"])
175
175
 
176
176
  if downloader_cfg.login_required and fetcher.is_logged_in:
177
177
  await fetcher.save_state()
@@ -127,6 +127,7 @@ class ConfigAdapter:
127
127
  site_cfg = self._get_site_cfg()
128
128
  return ParserConfig(
129
129
  cache_dir=gen.get("cache_dir", "./novel_cache"),
130
+ use_truncation=site_cfg.get("use_truncation", True),
130
131
  decode_font=font_ocr.get("decode_font", False),
131
132
  use_freq=font_ocr.get("use_freq", False),
132
133
  use_ocr=font_ocr.get("use_ocr", True),
@@ -14,8 +14,26 @@ downloading and processing online novel content, including:
14
14
  - Exporter: Responsible for exporting downloaded data into various output formats.
15
15
  """
16
16
 
17
- from .factory import get_parser
17
+ from .factory import (
18
+ get_downloader,
19
+ get_exporter,
20
+ get_fetcher,
21
+ get_parser,
22
+ )
23
+ from .interfaces import (
24
+ DownloaderProtocol,
25
+ ExporterProtocol,
26
+ FetcherProtocol,
27
+ ParserProtocol,
28
+ )
18
29
 
19
30
  __all__ = [
31
+ "get_downloader",
32
+ "get_exporter",
33
+ "get_fetcher",
20
34
  "get_parser",
35
+ "DownloaderProtocol",
36
+ "ExporterProtocol",
37
+ "FetcherProtocol",
38
+ "ParserProtocol",
21
39
  ]
@@ -15,7 +15,6 @@ from typing import Any
15
15
 
16
16
  from novel_downloader.core.interfaces import (
17
17
  DownloaderProtocol,
18
- ExporterProtocol,
19
18
  FetcherProtocol,
20
19
  ParserProtocol,
21
20
  )
@@ -34,13 +33,11 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
34
33
  self,
35
34
  fetcher: FetcherProtocol,
36
35
  parser: ParserProtocol,
37
- exporter: ExporterProtocol,
38
36
  config: DownloaderConfig,
39
37
  site: str,
40
38
  ):
41
39
  self._fetcher = fetcher
42
40
  self._parser = parser
43
- self._exporter = exporter
44
41
  self._config = config
45
42
  self._site = site
46
43
 
@@ -158,10 +155,6 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
158
155
  def parser(self) -> ParserProtocol:
159
156
  return self._parser
160
157
 
161
- @property
162
- def exporter(self) -> ExporterProtocol:
163
- return self._exporter
164
-
165
158
  @property
166
159
  def config(self) -> DownloaderConfig:
167
160
  return self._config
@@ -7,7 +7,6 @@ novel_downloader.core.downloaders.biquge
7
7
 
8
8
  from novel_downloader.core.downloaders.common import CommonDownloader
9
9
  from novel_downloader.core.interfaces import (
10
- ExporterProtocol,
11
10
  FetcherProtocol,
12
11
  ParserProtocol,
13
12
  )
@@ -21,7 +20,6 @@ class BiqugeDownloader(CommonDownloader):
21
20
  self,
22
21
  fetcher: FetcherProtocol,
23
22
  parser: ParserProtocol,
24
- exporter: ExporterProtocol,
25
23
  config: DownloaderConfig,
26
24
  ):
27
- super().__init__(fetcher, parser, exporter, config, "biquge")
25
+ super().__init__(fetcher, parser, config, "biquge")
@@ -440,8 +440,6 @@ class CommonDownloader(BaseDownloader):
440
440
  normal_cs.close()
441
441
  save_as_json(book_info, info_path)
442
442
 
443
- await asyncio.to_thread(self.exporter.export, book_id)
444
-
445
443
  self.logger.info(
446
444
  "%s Novel '%s' download completed.",
447
445
  TAG,
@@ -7,7 +7,6 @@ novel_downloader.core.downloaders.esjzone
7
7
 
8
8
  from novel_downloader.core.downloaders.common import CommonDownloader
9
9
  from novel_downloader.core.interfaces import (
10
- ExporterProtocol,
11
10
  FetcherProtocol,
12
11
  ParserProtocol,
13
12
  )
@@ -21,7 +20,6 @@ class EsjzoneDownloader(CommonDownloader):
21
20
  self,
22
21
  fetcher: FetcherProtocol,
23
22
  parser: ParserProtocol,
24
- exporter: ExporterProtocol,
25
23
  config: DownloaderConfig,
26
24
  ):
27
- super().__init__(fetcher, parser, exporter, config, "esjzone")
25
+ super().__init__(fetcher, parser, config, "esjzone")
@@ -7,7 +7,6 @@ novel_downloader.core.downloaders.linovelib
7
7
 
8
8
  from novel_downloader.core.downloaders.common import CommonDownloader
9
9
  from novel_downloader.core.interfaces import (
10
- ExporterProtocol,
11
10
  FetcherProtocol,
12
11
  ParserProtocol,
13
12
  )
@@ -21,7 +20,6 @@ class LinovelibDownloader(CommonDownloader):
21
20
  self,
22
21
  fetcher: FetcherProtocol,
23
22
  parser: ParserProtocol,
24
- exporter: ExporterProtocol,
25
23
  config: DownloaderConfig,
26
24
  ):
27
- super().__init__(fetcher, parser, exporter, config, "linovelib")
25
+ super().__init__(fetcher, parser, config, "linovelib")
@@ -7,7 +7,6 @@ novel_downloader.core.downloaders.qianbi
7
7
 
8
8
  from novel_downloader.core.downloaders.common import CommonDownloader
9
9
  from novel_downloader.core.interfaces import (
10
- ExporterProtocol,
11
10
  FetcherProtocol,
12
11
  ParserProtocol,
13
12
  )
@@ -21,7 +20,6 @@ class QianbiDownloader(CommonDownloader):
21
20
  self,
22
21
  fetcher: FetcherProtocol,
23
22
  parser: ParserProtocol,
24
- exporter: ExporterProtocol,
25
23
  config: DownloaderConfig,
26
24
  ):
27
- super().__init__(fetcher, parser, exporter, config, "qianbi")
25
+ super().__init__(fetcher, parser, config, "qianbi")
@@ -13,7 +13,6 @@ from typing import Any, cast
13
13
 
14
14
  from novel_downloader.core.downloaders.base import BaseDownloader
15
15
  from novel_downloader.core.interfaces import (
16
- ExporterProtocol,
17
16
  FetcherProtocol,
18
17
  ParserProtocol,
19
18
  )
@@ -41,11 +40,10 @@ class QidianDownloader(BaseDownloader):
41
40
  self,
42
41
  fetcher: FetcherProtocol,
43
42
  parser: ParserProtocol,
44
- exporter: ExporterProtocol,
45
43
  config: DownloaderConfig,
46
44
  ):
47
45
  config.request_interval = max(1.0, config.request_interval)
48
- super().__init__(fetcher, parser, exporter, config, "qidian")
46
+ super().__init__(fetcher, parser, config, "qidian")
49
47
 
50
48
  async def _download_one(
51
49
  self,
@@ -351,8 +349,6 @@ class QidianDownloader(BaseDownloader):
351
349
  normal_cs.close()
352
350
  encrypted_cs.close()
353
351
 
354
- await asyncio.to_thread(self.exporter.export, book_id)
355
-
356
352
  self.logger.info(
357
353
  "%s Novel '%s' download completed.",
358
354
  TAG,
@@ -7,7 +7,6 @@ novel_downloader.core.downloaders.sfacg
7
7
 
8
8
  from novel_downloader.core.downloaders.common import CommonDownloader
9
9
  from novel_downloader.core.interfaces import (
10
- ExporterProtocol,
11
10
  FetcherProtocol,
12
11
  ParserProtocol,
13
12
  )
@@ -21,7 +20,6 @@ class SfacgDownloader(CommonDownloader):
21
20
  self,
22
21
  fetcher: FetcherProtocol,
23
22
  parser: ParserProtocol,
24
- exporter: ExporterProtocol,
25
23
  config: DownloaderConfig,
26
24
  ):
27
- super().__init__(fetcher, parser, exporter, config, "sfacg")
25
+ super().__init__(fetcher, parser, config, "sfacg")
@@ -7,7 +7,6 @@ novel_downloader.core.downloaders.yamibo
7
7
 
8
8
  from novel_downloader.core.downloaders.common import CommonDownloader
9
9
  from novel_downloader.core.interfaces import (
10
- ExporterProtocol,
11
10
  FetcherProtocol,
12
11
  ParserProtocol,
13
12
  )
@@ -21,7 +20,6 @@ class YamiboDownloader(CommonDownloader):
21
20
  self,
22
21
  fetcher: FetcherProtocol,
23
22
  parser: ParserProtocol,
24
- exporter: ExporterProtocol,
25
23
  config: DownloaderConfig,
26
24
  ):
27
- super().__init__(fetcher, parser, exporter, config, "yamibo")
25
+ super().__init__(fetcher, parser, config, "yamibo")
@@ -22,14 +22,13 @@ from novel_downloader.core.downloaders import (
22
22
  )
23
23
  from novel_downloader.core.interfaces import (
24
24
  DownloaderProtocol,
25
- ExporterProtocol,
26
25
  FetcherProtocol,
27
26
  ParserProtocol,
28
27
  )
29
28
  from novel_downloader.models import DownloaderConfig
30
29
 
31
30
  DownloaderBuilder = Callable[
32
- [FetcherProtocol, ParserProtocol, ExporterProtocol, DownloaderConfig],
31
+ [FetcherProtocol, ParserProtocol, DownloaderConfig],
33
32
  DownloaderProtocol,
34
33
  ]
35
34
 
@@ -47,7 +46,6 @@ _site_map: dict[str, DownloaderBuilder] = {
47
46
  def get_downloader(
48
47
  fetcher: FetcherProtocol,
49
48
  parser: ParserProtocol,
50
- exporter: ExporterProtocol,
51
49
  site: str,
52
50
  config: DownloaderConfig,
53
51
  ) -> DownloaderProtocol:
@@ -56,7 +54,6 @@ def get_downloader(
56
54
 
57
55
  :param fetcher: Fetcher implementation
58
56
  :param parser: Parser implementation
59
- :param exporter: Exporter implementation
60
57
  :param site: Site name (e.g., 'qidian')
61
58
  :param config: Downloader configuration
62
59
 
@@ -66,11 +63,11 @@ def get_downloader(
66
63
 
67
64
  # site-specific
68
65
  if site_key in _site_map:
69
- return _site_map[site_key](fetcher, parser, exporter, config)
66
+ return _site_map[site_key](fetcher, parser, config)
70
67
 
71
68
  # fallback
72
69
  site_rules = load_site_rules()
73
70
  if site_key not in site_rules:
74
71
  raise ValueError(f"Unsupported site: {site}")
75
72
 
76
- return CommonDownloader(fetcher, parser, exporter, config, site_key)
73
+ return CommonDownloader(fetcher, parser, config, site_key)
@@ -201,19 +201,9 @@ class BaseBrowser(FetcherProtocol, abc.ABC):
201
201
  **kwargs: Any,
202
202
  ) -> str:
203
203
  if self._reuse_page:
204
- if not self._page:
205
- self._page = await self.context.new_page()
206
- page = self._page
204
+ return await self._fetch_with_reuse(url, wait_until, referer, **kwargs)
207
205
  else:
208
- page = await self.context.new_page()
209
-
210
- await page.goto(url, wait_until=wait_until, referer=referer)
211
- content = await page.content()
212
-
213
- if not self._reuse_page:
214
- await page.close()
215
-
216
- return str(content)
206
+ return await self._fetch_with_new(url, wait_until, referer, **kwargs)
217
207
 
218
208
  async def load_state(self) -> bool:
219
209
  """ """
@@ -286,6 +276,36 @@ class BaseBrowser(FetcherProtocol, abc.ABC):
286
276
  await self.init(headless=headless)
287
277
  self.logger.debug("[browser] Browser restarted (headless=%s).", headless)
288
278
 
279
+ async def _fetch_with_new(
280
+ self,
281
+ url: str,
282
+ wait_until: Literal["commit", "domcontentloaded", "load", "networkidle"]
283
+ | None = "load",
284
+ referer: str | None = None,
285
+ **kwargs: Any,
286
+ ) -> str:
287
+ page = await self.context.new_page()
288
+ try:
289
+ await page.goto(url, wait_until=wait_until, referer=referer, **kwargs)
290
+ html: str = await page.content()
291
+ return html
292
+ finally:
293
+ await page.close()
294
+
295
+ async def _fetch_with_reuse(
296
+ self,
297
+ url: str,
298
+ wait_until: Literal["commit", "domcontentloaded", "load", "networkidle"]
299
+ | None = "load",
300
+ referer: str | None = None,
301
+ **kwargs: Any,
302
+ ) -> str:
303
+ if not self._page:
304
+ self._page = await self.context.new_page()
305
+ await self._page.goto(url, wait_until=wait_until, referer=referer, **kwargs)
306
+ html: str = await self._page.content()
307
+ return html
308
+
289
309
  @property
290
310
  def hostname(self) -> str:
291
311
  return ""
@@ -49,15 +49,17 @@ class EsjzoneBrowser(BaseBrowser):
49
49
 
50
50
  login_page = await self.context.new_page()
51
51
 
52
- await login_page.goto(self.API_LOGIN_URL_1, wait_until="networkidle")
52
+ try:
53
+ await login_page.goto(self.API_LOGIN_URL_1, wait_until="networkidle")
53
54
 
54
- await login_page.fill('input[name="email"]', username)
55
- await login_page.fill('input[name="pwd"]', password)
55
+ await login_page.fill('input[name="email"]', username)
56
+ await login_page.fill('input[name="pwd"]', password)
56
57
 
57
- await login_page.click('a.btn-send[data-send="mem_login"]')
58
+ await login_page.click('a.btn-send[data-send="mem_login"]')
58
59
 
59
- await login_page.wait_for_load_state("networkidle")
60
- await login_page.close()
60
+ await login_page.wait_for_load_state("networkidle")
61
+ finally:
62
+ await login_page.close()
61
63
 
62
64
  self._is_logged_in = await self._check_login_status()
63
65
 
@@ -22,8 +22,8 @@ class QidianBrowser(BaseBrowser):
22
22
 
23
23
  HOMEPAGE_URL = "https://www.qidian.com/"
24
24
  BOOKCASE_URL = "https://my.qidian.com/bookcase/"
25
- BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
26
- # BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
25
+ # BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
26
+ BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
27
27
  CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
28
28
 
29
29
  LOGIN_URL = "https://passport.qidian.com/"
@@ -27,7 +27,8 @@ class QidianSession(BaseSession):
27
27
 
28
28
  HOMEPAGE_URL = "https://www.qidian.com/"
29
29
  BOOKCASE_URL = "https://my.qidian.com/bookcase/"
30
- BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
30
+ # BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
31
+ BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
31
32
  CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
32
33
 
33
34
  LOGIN_URL = "https://passport.qidian.com/"
@@ -48,8 +48,8 @@ class YamiboBrowser(BaseBrowser):
48
48
  return False
49
49
 
50
50
  for i in range(1, attempt + 1):
51
+ login_page = await self.context.new_page()
51
52
  try:
52
- login_page = await self.context.new_page()
53
53
  await login_page.goto(self.LOGIN_URL, wait_until="networkidle")
54
54
 
55
55
  await login_page.fill("#loginform-username", username)
@@ -68,8 +68,6 @@ class YamiboBrowser(BaseBrowser):
68
68
  f"[auth] No URL change after login attempt {i}: {e}"
69
69
  )
70
70
 
71
- await login_page.close()
72
-
73
71
  self._is_logged_in = await self._check_login_status()
74
72
  if self._is_logged_in:
75
73
  self.logger.info(f"[auth] Login successful on attempt {i}.")
@@ -83,6 +81,8 @@ class YamiboBrowser(BaseBrowser):
83
81
  self.logger.error(
84
82
  f"[auth] Unexpected error during login attempt {i}: {e}"
85
83
  )
84
+ finally:
85
+ await login_page.close()
86
86
 
87
87
  self.logger.error(f"[auth] Login failed after {attempt} attempt(s).")
88
88
  return False
@@ -10,36 +10,19 @@ time, status, word count, summary, and volume-chapter structure.
10
10
  """
11
11
 
12
12
  import logging
13
+ import re
14
+ from datetime import datetime
13
15
  from typing import Any
14
16
 
15
17
  from lxml import html
16
18
 
17
19
  logger = logging.getLogger(__name__)
18
20
 
19
- _AUTHOR_XPATH = (
20
- 'string(//div[contains(@class, "book-info")]//a[contains(@class, "writer")])'
21
- )
22
-
23
21
 
24
22
  def _chapter_url_to_id(url: str) -> str:
25
23
  return url.rstrip("/").split("/")[-1]
26
24
 
27
25
 
28
- def _get_volume_name(
29
- vol_elem: html.HtmlElement,
30
- ) -> str:
31
- """
32
- Extracts the volume title from a <div class="volume"> element using lxml.
33
- Ignores <a> tags, and extracts text from other elements.
34
- """
35
- h3_candidates = vol_elem.xpath(".//h3")
36
- if not h3_candidates:
37
- return ""
38
- texts = vol_elem.xpath(".//h3//text()[not(ancestor::a)]")
39
- full_text = "".join(texts).strip()
40
- return full_text.split(chr(183))[0].strip()
41
-
42
-
43
26
  def parse_book_info(html_str: str) -> dict[str, Any]:
44
27
  """
45
28
  Extract metadata: title, author, cover_url, update_time, status,
@@ -52,59 +35,53 @@ def parse_book_info(html_str: str) -> dict[str, Any]:
52
35
  try:
53
36
  doc = html.fromstring(html_str)
54
37
 
55
- book_name = doc.xpath('string(//h1/em[@id="bookName"])').strip()
56
- info["book_name"] = book_name
38
+ info["book_name"] = doc.xpath('string(//h1[@id="bookName"])').strip()
57
39
 
58
- author = doc.xpath(_AUTHOR_XPATH).strip()
59
- info["author"] = author
40
+ info["author"] = doc.xpath('string(//a[@class="writer-name"])').strip()
60
41
 
61
- cover_url = doc.xpath('string(//div[@class="book-img"]//img/@src)').strip()
62
- info["cover_url"] = cover_url
42
+ book_id = doc.xpath('//a[@id="bookImg"]/@data-bid')[0]
43
+ info[
44
+ "cover_url"
45
+ ] = f"https://bookcover.yuewen.com/qdbimg/349573/{book_id}/600.webp"
63
46
 
64
- update_raw = (
65
- doc.xpath('string(//span[contains(@class, "update-time")])')
66
- .replace("更新时间", "")
47
+ ut = (
48
+ doc.xpath('string(//span[@class="update-time"])')
49
+ .replace("更新时间:", "")
67
50
  .strip()
68
51
  )
69
- info["update_time"] = update_raw
52
+ if re.match(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$", ut):
53
+ info["update_time"] = ut
54
+ else:
55
+ info["update_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
70
56
 
71
- status = doc.xpath('string(//p[@class="tag"]/span[@class="blue"][1])').strip()
72
- info["serial_status"] = status
57
+ info["serial_status"] = doc.xpath(
58
+ 'string(//p[@class="book-attribute"]/span[1])'
59
+ ).strip()
73
60
 
74
- tags = doc.xpath('//p[@class="tag"]/a[@class="red"]/text()')
61
+ tags = doc.xpath('//p[contains(@class,"all-label")]//a/text()')
75
62
  info["tags"] = [t.strip() for t in tags if t.strip()]
76
63
 
77
- wc_number = doc.xpath("string(//p[em and cite][1]/em[1])").strip()
78
- wc_unit = doc.xpath("string(//p[em and cite][1]/cite[1])").strip()
79
- info["word_count"] = (
80
- (wc_number + wc_unit) if wc_number and wc_unit else "Unknown"
81
- )
64
+ info["word_count"] = doc.xpath('string(//p[@class="count"]/em[1])').strip()
82
65
 
83
66
  summary = doc.xpath('string(//p[@class="intro"])').strip()
84
67
  info["summary_brief"] = summary
85
68
 
86
- intro_list = doc.xpath('//div[@class="book-intro"]/p')[0]
87
- detail_intro = "\n".join(intro_list.itertext()).strip()
88
- info["summary"] = detail_intro
69
+ raw = doc.xpath('//p[@id="book-intro-detail"]//text()')
70
+ info["summary"] = "\n".join(line.strip() for line in raw if line.strip())
89
71
 
90
72
  volumes = []
91
- for vol_div in doc.xpath('//div[@class="volume-wrap"]/div[@class="volume"]'):
92
- volume_name = _get_volume_name(vol_div)
73
+ for vol in doc.xpath('//div[@id="allCatalog"]//div[@class="catalog-volume"]'):
74
+ vol_name = vol.xpath('string(.//h3[@class="volume-name"])').strip()
75
+ vol_name = vol_name.split(chr(183))[0].strip()
93
76
  chapters = []
94
- for li in vol_div.xpath(".//li"):
95
- a = li.xpath(".//a")[0] if li.xpath(".//a") else None
96
- if a is None or "href" not in a.attrib:
97
- continue
98
- href = a.attrib["href"].strip()
99
- title = "".join(a.itertext()).strip()
77
+ for li in vol.xpath('.//ul[contains(@class,"volume-chapters")]/li'):
78
+ a = li.xpath('.//a[@class="chapter-name"]')[0]
79
+ title = a.text.strip()
80
+ url = a.get("href")
100
81
  chapters.append(
101
- {
102
- "title": title,
103
- "url": href,
104
- "chapterId": _chapter_url_to_id(href),
105
- }
82
+ {"title": title, "url": url, "chapterId": _chapter_url_to_id(url)}
106
83
  )
107
- volumes.append({"volume_name": volume_name, "chapters": chapters})
84
+ volumes.append({"volume_name": vol_name, "chapters": chapters})
108
85
  info["volumes"] = volumes
109
86
 
110
87
  except Exception as e:
@@ -19,12 +19,16 @@ from lxml import html
19
19
 
20
20
  from novel_downloader.models import ChapterDict
21
21
  from novel_downloader.utils.network import download_font_file
22
- from novel_downloader.utils.text_utils import apply_font_mapping
22
+ from novel_downloader.utils.text_utils import (
23
+ apply_font_mapping,
24
+ truncate_half_lines,
25
+ )
23
26
 
24
27
  from .utils import (
25
28
  extract_chapter_info,
26
29
  find_ssr_page_context,
27
30
  get_decryptor,
31
+ is_duplicated,
28
32
  vip_status,
29
33
  )
30
34
 
@@ -76,6 +80,7 @@ def parse_encrypted_chapter(
76
80
  fixedFontWoff2_url = chapter_info["fixedFontWoff2"]
77
81
 
78
82
  title = chapter_info.get("chapterName", "Untitled")
83
+ duplicated = is_duplicated(ssr_data)
79
84
  raw_html = chapter_info.get("content", "")
80
85
  chapter_id = chapter_info.get("chapterId", chapter_id)
81
86
  fkp = chapter_info.get("fkp", "")
@@ -83,7 +88,7 @@ def parse_encrypted_chapter(
83
88
  update_time = chapter_info.get("updateTime", "")
84
89
  update_timestamp = chapter_info.get("updateTimestamp", 0)
85
90
  modify_time = chapter_info.get("modifyTime", 0)
86
- word_count = chapter_info.get("wordsCount", 0)
91
+ word_count = chapter_info.get("actualWords", 0)
87
92
  seq = chapter_info.get("seq", None)
88
93
  volume = chapter_info.get("extra", {}).get("volumeName", "")
89
94
 
@@ -177,6 +182,9 @@ def parse_encrypted_chapter(
177
182
  final_paragraphs_str = "\n\n".join(
178
183
  line.strip() for line in original_text.splitlines() if line.strip()
179
184
  )
185
+ if parser._use_truncation and duplicated:
186
+ final_paragraphs_str = truncate_half_lines(final_paragraphs_str)
187
+
180
188
  return {
181
189
  "id": str(chapter_id),
182
190
  "title": str(title),
@@ -187,6 +195,7 @@ def parse_encrypted_chapter(
187
195
  "update_timestamp": update_timestamp,
188
196
  "modify_time": modify_time,
189
197
  "word_count": word_count,
198
+ "duplicated": duplicated,
190
199
  "seq": seq,
191
200
  "volume": volume,
192
201
  "encrypted": True,
@@ -15,11 +15,13 @@ from typing import TYPE_CHECKING
15
15
  from lxml import html
16
16
 
17
17
  from novel_downloader.models import ChapterDict
18
+ from novel_downloader.utils.text_utils import truncate_half_lines
18
19
 
19
20
  from .utils import (
20
21
  extract_chapter_info,
21
22
  find_ssr_page_context,
22
23
  get_decryptor,
24
+ is_duplicated,
23
25
  vip_status,
24
26
  )
25
27
 
@@ -51,6 +53,7 @@ def parse_normal_chapter(
51
53
  return None
52
54
 
53
55
  title = chapter_info.get("chapterName", "Untitled")
56
+ duplicated = is_duplicated(ssr_data)
54
57
  raw_html = chapter_info.get("content", "")
55
58
  chapter_id = chapter_info.get("chapterId", chapter_id)
56
59
  fkp = chapter_info.get("fkp", "")
@@ -58,7 +61,7 @@ def parse_normal_chapter(
58
61
  update_time = chapter_info.get("updateTime", "")
59
62
  update_timestamp = chapter_info.get("updateTimestamp", 0)
60
63
  modify_time = chapter_info.get("modifyTime", 0)
61
- word_count = chapter_info.get("wordsCount", 0)
64
+ word_count = chapter_info.get("actualWords", 0)
62
65
  seq = chapter_info.get("seq", None)
63
66
  volume = chapter_info.get("extra", {}).get("volumeName", "")
64
67
 
@@ -74,6 +77,9 @@ def parse_normal_chapter(
74
77
  if not chapter_text:
75
78
  return None
76
79
 
80
+ if parser._use_truncation and duplicated:
81
+ chapter_text = truncate_half_lines(chapter_text)
82
+
77
83
  return {
78
84
  "id": str(chapter_id),
79
85
  "title": title,
@@ -84,6 +90,7 @@ def parse_normal_chapter(
84
90
  "update_timestamp": update_timestamp,
85
91
  "modify_time": modify_time,
86
92
  "word_count": word_count,
93
+ "duplicated": duplicated,
87
94
  "seq": seq,
88
95
  "volume": volume,
89
96
  "encrypted": False,
@@ -32,7 +32,11 @@ class QidianParser(BaseParser):
32
32
  Parser for Qidian site.
33
33
  """
34
34
 
35
- def __init__(self, config: ParserConfig):
35
+ def __init__(
36
+ self,
37
+ config: ParserConfig,
38
+ fuid: str = "",
39
+ ):
36
40
  """
37
41
  Initialize the QidianParser with the given configuration.
38
42
 
@@ -41,6 +45,7 @@ class QidianParser(BaseParser):
41
45
  super().__init__(config)
42
46
 
43
47
  # Extract and store parser flags from config
48
+ self._use_truncation = config.use_truncation
44
49
  self._decode_font: bool = config.decode_font
45
50
  self._save_font_debug: bool = config.save_font_debug
46
51
 
@@ -52,7 +57,7 @@ class QidianParser(BaseParser):
52
57
  DATA_DIR / "qidian" / "browser_state.cookies",
53
58
  DATA_DIR / "qidian" / "session_state.cookies",
54
59
  ]
55
- self._fuid: str = find_cookie_value(state_files, "ywguid")
60
+ self._fuid: str = fuid or find_cookie_value(state_files, "ywguid")
56
61
 
57
62
  self._font_ocr: FontOCR | None = None
58
63
  if self._decode_font:
@@ -9,6 +9,7 @@ from .helpers import (
9
9
  can_view_chapter,
10
10
  extract_chapter_info,
11
11
  find_ssr_page_context,
12
+ is_duplicated,
12
13
  is_encrypted,
13
14
  is_restricted_page,
14
15
  vip_status,
@@ -22,6 +23,7 @@ __all__ = [
22
23
  "vip_status",
23
24
  "can_view_chapter",
24
25
  "is_encrypted",
26
+ "is_duplicated",
25
27
  "QidianNodeDecryptor",
26
28
  "get_decryptor",
27
29
  ]
@@ -89,6 +89,15 @@ def can_view_chapter(ssr_data: dict[str, Any]) -> bool:
89
89
  return not (vip_status == 1 and is_buy == 0)
90
90
 
91
91
 
92
+ def is_duplicated(ssr_data: dict[str, Any]) -> bool:
93
+ """
94
+ Check if chapter is marked as duplicated (eFW = 1).
95
+ """
96
+ chapter_info = extract_chapter_info(ssr_data)
97
+ efw_flag = chapter_info.get("eFW", 0)
98
+ return bool(efw_flag == 1)
99
+
100
+
92
101
  def is_encrypted(content: str | dict[str, Any]) -> bool:
93
102
  """
94
103
  Return True if content is encrypted.
@@ -68,6 +68,7 @@ class DownloaderConfig:
68
68
  @dataclass
69
69
  class ParserConfig:
70
70
  cache_dir: str = "./novel_cache"
71
+ use_truncation: bool = True
71
72
  decode_font: bool = False
72
73
  use_freq: bool = False
73
74
  use_ocr: bool = True
@@ -52,6 +52,7 @@ book_ids = [
52
52
  ]
53
53
  mode = "session" # browser / session
54
54
  login_required = true # 是否需要登录才能访问
55
+ use_truncation = true # 是否基于章节长度截断以避免重复内容
55
56
 
56
57
  [sites.biquge] # 笔趣阁
57
58
  book_ids = [
@@ -65,7 +65,13 @@ class HomeScreen(Screen): # type: ignore[misc]
65
65
  return
66
66
  id_list = {x.strip() for x in ids.split(",") if x.strip()}
67
67
  adapter = ConfigAdapter(config=self.app.config, site=str(site))
68
- asyncio.create_task(self._download(adapter, str(site), id_list))
68
+ # asyncio.create_task(self._download(adapter, str(site), id_list))
69
+ self.run_worker(
70
+ self._download(adapter, str(site), id_list),
71
+ name="download",
72
+ group="downloads",
73
+ description="正在下载书籍...",
74
+ )
69
75
 
70
76
  def _make_title_bar(self) -> Horizontal:
71
77
  return Horizontal(
@@ -134,7 +140,6 @@ class HomeScreen(Screen): # type: ignore[misc]
134
140
  downloader = get_downloader(
135
141
  fetcher=fetcher,
136
142
  parser=parser,
137
- exporter=exporter,
138
143
  site=site,
139
144
  config=downloader_cfg,
140
145
  )
@@ -145,6 +150,7 @@ class HomeScreen(Screen): # type: ignore[misc]
145
150
  {"book_id": book_id},
146
151
  progress_hook=self._update_progress,
147
152
  )
153
+ await asyncio.to_thread(exporter.export, book_id)
148
154
 
149
155
  if downloader_cfg.login_required and fetcher.is_logged_in:
150
156
  await fetcher.save_state()
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  novel_downloader.tui.screens.login
4
- ---------------------------------
4
+ ----------------------------------
5
5
 
6
6
  """
7
7
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.utils.model_loader
4
- -----------------------------------
3
+ novel_downloader.utils.fontocr.model_loader
4
+ -------------------------------------------
5
5
 
6
6
  Utility functions for managing pre-trained model downloads.
7
7
 
@@ -25,7 +25,8 @@ from novel_downloader.utils.constants import (
25
25
  REC_IMAGE_SHAPE_MAP,
26
26
  )
27
27
  from novel_downloader.utils.hash_store import img_hash_store
28
- from novel_downloader.utils.model_loader import get_rec_chinese_char_model_dir
28
+
29
+ from .model_loader import get_rec_chinese_char_model_dir
29
30
 
30
31
  logger = logging.getLogger(__name__)
31
32
 
@@ -36,7 +36,8 @@ from novel_downloader.utils.constants import (
36
36
  REC_IMAGE_SHAPE_MAP,
37
37
  )
38
38
  from novel_downloader.utils.hash_store import img_hash_store
39
- from novel_downloader.utils.model_loader import (
39
+
40
+ from .model_loader import (
40
41
  get_rec_char_vector_dir,
41
42
  get_rec_chinese_char_model_dir,
42
43
  )
@@ -15,12 +15,19 @@ Submodules:
15
15
  from .chapter_formatting import format_chapter
16
16
  from .diff_display import diff_inline_display
17
17
  from .font_mapping import apply_font_mapping
18
- from .text_cleaning import clean_chapter_title, is_promotional_line
18
+ from .text_cleaning import (
19
+ clean_chapter_title,
20
+ content_prefix,
21
+ is_promotional_line,
22
+ truncate_half_lines,
23
+ )
19
24
 
20
25
  __all__ = [
21
26
  "apply_font_mapping",
22
27
  "format_chapter",
23
28
  "clean_chapter_title",
24
29
  "is_promotional_line",
30
+ "content_prefix",
31
+ "truncate_half_lines",
25
32
  "diff_inline_display",
26
33
  ]
@@ -6,6 +6,7 @@ novel_downloader.utils.text_utils.text_cleaning
6
6
  Tools for detecting and removing promotional or ad-like content from text.
7
7
  """
8
8
 
9
+ import math
9
10
  import re
10
11
 
11
12
  from novel_downloader.utils.file_utils.io import load_blacklisted_words
@@ -50,7 +51,57 @@ def is_promotional_line(line: str) -> bool:
50
51
  return False
51
52
 
52
53
 
54
+ def content_prefix(
55
+ text: str,
56
+ n: int,
57
+ ignore_chars: set[str] | None = None,
58
+ ) -> str:
59
+ """
60
+ Return the prefix of `text` containing the first `n` non-ignored characters.
61
+
62
+ :param text: The full input string.
63
+ :param n: Number of content characters to include.
64
+ :param ignore_chars: Characters to ignore when counting content.
65
+ :return: Truncated string preserving original whitespace and line breaks.
66
+ """
67
+ ignore = ignore_chars or set()
68
+ cnt = 0
69
+
70
+ for i, ch in enumerate(text):
71
+ if ch not in ignore:
72
+ cnt += 1
73
+ if cnt >= n:
74
+ return text[: i + 1]
75
+
76
+ return text
77
+
78
+
79
+ def truncate_half_lines(text: str) -> str:
80
+ """
81
+ Keep the first half of the lines (rounded up), preserving line breaks.
82
+
83
+ :param text: Full input text
84
+ :return: Truncated text with first half of lines
85
+ """
86
+ lines = text.splitlines()
87
+ non_empty_lines = [line for line in lines if line.strip()]
88
+ keep_count = math.ceil(len(non_empty_lines) / 2)
89
+
90
+ result_lines = []
91
+ count = 0
92
+ for line in lines:
93
+ result_lines.append(line)
94
+ if line.strip():
95
+ count += 1
96
+ if count >= keep_count:
97
+ break
98
+
99
+ return "\n".join(result_lines)
100
+
101
+
53
102
  __all__ = [
54
103
  "clean_chapter_title",
55
104
  "is_promotional_line",
105
+ "content_prefix",
106
+ "truncate_half_lines",
56
107
  ]
@@ -138,7 +138,7 @@ def calculate_time_difference(
138
138
 
139
139
  except Exception as e:
140
140
  logger.warning("[time] Failed to calculate time difference: %s", e)
141
- return 0, 0, 0, 0
141
+ return 999, 23, 59, 59
142
142
 
143
143
 
144
144
  __all__ = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: novel-downloader
3
- Version: 1.4.2
3
+ Version: 1.4.4
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
@@ -182,6 +182,7 @@ pip install .
182
182
  - [CLI 使用示例](https://github.com/BowenZ217/novel-downloader/blob/main/docs/6-cli-usage-examples.md)
183
183
  - [复制 Cookies](https://github.com/BowenZ217/novel-downloader/blob/main/docs/copy-cookies.md)
184
184
  - [文件保存](https://github.com/BowenZ217/novel-downloader/blob/main/docs/file-saving.md)
185
+ - [模块与接口文档](https://github.com/BowenZ217/novel-downloader/blob/main/docs/api/README.md)
185
186
  - [TODO](https://github.com/BowenZ217/novel-downloader/blob/main/docs/todo.md)
186
187
  - [开发](https://github.com/BowenZ217/novel-downloader/blob/main/docs/develop.md)
187
188
  - [项目说明](#项目说明)
@@ -1,25 +1,25 @@
1
- novel_downloader/__init__.py,sha256=qYZxJQ-xYLPTjMwE7FYcQM5HYV9Oo_bqCJY55hAqlcE,218
1
+ novel_downloader/__init__.py,sha256=dWsU4eiEbY9H2_GSR5Y5XdVhEH4CyEbDyc7rzVr79qI,218
2
2
  novel_downloader/cli/__init__.py,sha256=-2HAut_U1e67MZGdvbpEJ1n5J-bRchzto6L4c-nWeXY,174
3
3
  novel_downloader/cli/clean.py,sha256=hOk8SJQwBCw2oOObTdEI79wpnmZ25uB1s9LQK1-4LNU,4487
4
4
  novel_downloader/cli/config.py,sha256=C6QLfegZLp4legmu8KenqyYKNdrk47bH0z86ujLP0pY,6509
5
- novel_downloader/cli/download.py,sha256=ff2KMsa9XnbsF_aUQcAc7a06hDYmk4-7yMv4iiXTWlI,6762
5
+ novel_downloader/cli/download.py,sha256=wj6wW4osxdB0VA0VeO5Tt2f6HY5CL0Gp8BeoaMzb9Is,6801
6
6
  novel_downloader/cli/export.py,sha256=x9uvyLuvkuaDZGoH212aHZ7XyPT9b2S78AmTN6rkAu4,2283
7
7
  novel_downloader/cli/main.py,sha256=9J8KMuYwL01X6chIaXpQNeS5d3pHnwB9vA9XjKd8RrM,919
8
8
  novel_downloader/config/__init__.py,sha256=2mnf33MQOUnLGCnL1NtNV_rHBejNxBNIbobIGN0tw4E,666
9
- novel_downloader/config/adapter.py,sha256=hPRAomAlqhyUlNemm82e4m0SAYa33woulb5ePHMRC3o,8330
9
+ novel_downloader/config/adapter.py,sha256=Kp_QOE-ntj5yWBYUnrdNj7ab-R9ZcmBNnMi_dHkv4Gs,8395
10
10
  novel_downloader/config/loader.py,sha256=jo_1rr3UKZRAFFYgO-oHpYLRhF431chmfx4fLGh0MKw,5743
11
11
  novel_downloader/config/site_rules.py,sha256=CJksBSvVAC4sR6cEruf4pM1Jv0zTJb1lcHq0Yn6LPFM,2979
12
- novel_downloader/core/__init__.py,sha256=zzrXjQfBUhfLmBD_95oHTjtTsR81NSRtHcxpYBxQlZ4,654
12
+ novel_downloader/core/__init__.py,sha256=sYwhveDjNQu0oKfS9obWVJGmbxyN8lIWIRiGvGOBZRI,989
13
13
  novel_downloader/core/downloaders/__init__.py,sha256=AK5zeetVXOn_irgHp-NORPYW65UJM4CsF8ysxf2vKD4,1064
14
- novel_downloader/core/downloaders/base.py,sha256=h2Y73o9OC8URvlnNNQgwNVx4kk4oqFjk2AsyfmAfshg,6706
15
- novel_downloader/core/downloaders/biquge.py,sha256=PTm8eeaHVyw_nvtnuRpZ2hT7hLs2LEPV1Drd2O6idNM,651
16
- novel_downloader/core/downloaders/common.py,sha256=-3lp6lLoCubVwqMA4lx6YtnSmKPMA85T1K5m0_vizmg,16323
17
- novel_downloader/core/downloaders/esjzone.py,sha256=GBFJ3fIPov483QxkOTaKBU2wd2f8T4GdvzSbRaU9hDI,655
18
- novel_downloader/core/downloaders/linovelib.py,sha256=Cd43_70SKbKM8_C8c4qev3pLUgyziW9413qrA8EcStY,663
19
- novel_downloader/core/downloaders/qianbi.py,sha256=ARuk1xABOGCjlD_ct5hOKMgWloU187rUJBbICJ2yvkY,651
20
- novel_downloader/core/downloaders/qidian.py,sha256=PkUnOEq1TyKZea--b8nBgFqrhxD0UK8X4m9AUC_wFjk,13135
21
- novel_downloader/core/downloaders/sfacg.py,sha256=ShlkIIR6OW_x2FhNDi79S_3AL2CWId2upQmRkApee_E,647
22
- novel_downloader/core/downloaders/yamibo.py,sha256=9L0m5Zq0o1y44tCQ4ZRHHqdO3YresJkRAVvCtsQwouk,651
14
+ novel_downloader/core/downloaders/base.py,sha256=aZECG_HkCPNOGoGY6n8hEue769J2tlAqHz5LHGSwwNw,6525
15
+ novel_downloader/core/downloaders/biquge.py,sha256=u1HkO3UvIq9lrQGJrU8UCBEqMnqPstL_AnfTRcK0J3c,583
16
+ novel_downloader/core/downloaders/common.py,sha256=TAojiAhVGXDaIxbCpAvB4FecLh1TSj8oQDuY9tNynC4,16259
17
+ novel_downloader/core/downloaders/esjzone.py,sha256=rRT2zMft-js2aOMxL71tsWR-to9FIIzua48DzKVwpgM,587
18
+ novel_downloader/core/downloaders/linovelib.py,sha256=tMRuZPRBBt-w0Te7A0Kc2Jjqz-Y_c3TmLhHxJSdxGXo,595
19
+ novel_downloader/core/downloaders/qianbi.py,sha256=ZuwRjMW-2VZRxJxUg3n3168hyhX779cLRC3VSlUYZBU,583
20
+ novel_downloader/core/downloaders/qidian.py,sha256=Zix-9dLMgt2kuMSOPbl9zdZ-TMawfOV5VkGRIrM6-T8,13003
21
+ novel_downloader/core/downloaders/sfacg.py,sha256=kEp3N4ycXJFNh3XqLfIV8MJzy9zi5OOH9XYlXDOO3nY,579
22
+ novel_downloader/core/downloaders/yamibo.py,sha256=oV4VpwCMtvWdEfDUijozId-LRZPlZkLlnkyyLx7_ZuM,583
23
23
  novel_downloader/core/exporters/__init__.py,sha256=ATkkdh6RUIaM19mG1XjFiaMnGRgFFGT3ixsqVkU13Q0,867
24
24
  novel_downloader/core/exporters/base.py,sha256=duIdLnj9kKNZ9r2aJV0Rzl-rnQCP8olk12uzduT9PtE,6146
25
25
  novel_downloader/core/exporters/biquge.py,sha256=SJCChYtDLJKrOpwDCT8IeR0v1LoiUQuTTejfGmRnxX8,462
@@ -38,13 +38,13 @@ novel_downloader/core/exporters/linovelib/epub.py,sha256=yVqv-lnc7Nh-COd4POvQ0vb
38
38
  novel_downloader/core/exporters/linovelib/main_exporter.py,sha256=cHkAp_jdF6uji5Avu1l8z6mR0nSrWYgX6ETDEezkjoE,3700
39
39
  novel_downloader/core/exporters/linovelib/txt.py,sha256=ALlZUl5nNtg4OmYlurMC0acjmTOBV7G8c13DPrxbG4w,4407
40
40
  novel_downloader/core/factory/__init__.py,sha256=_IY3N35onhWD_nw_TyxKOxa6e7Uak9Cv0bp4pK9yb0M,464
41
- novel_downloader/core/factory/downloader.py,sha256=hNCp3IlZQzeTSLBMuO_Y_EzrAn9_-8SLBHcUwMw8ijM,2060
41
+ novel_downloader/core/factory/downloader.py,sha256=M3yGIdHhthYdvUmXP-jJiVojmBcOf293q5YftgjWzfg,1923
42
42
  novel_downloader/core/factory/exporter.py,sha256=CjDJGnWBDk-S1zYntIDAEo1hLM2q55tlJOjTXKn0hAI,1533
43
43
  novel_downloader/core/factory/fetcher.py,sha256=stfRJnh5ZXLqRsDtQC1BDeTe0yaZI-mgm3Qx03nWUP4,2402
44
44
  novel_downloader/core/factory/parser.py,sha256=0PXepJhlE6aGs9_t81vyho1eCw84-6XRBGb98_phvSQ,2237
45
45
  novel_downloader/core/fetchers/__init__.py,sha256=C1OykEdCzj3fpLRRhVrvwClpzz-pzTzTilH306crgYg,1440
46
46
  novel_downloader/core/fetchers/base/__init__.py,sha256=p9be-q2YjiHcQhv4_KMeZmHgaAYYyUDWoBo6Gvwughc,224
47
- novel_downloader/core/fetchers/base/browser.py,sha256=oR3ZMw71_LJrgTZXduVtZuXEkr7PZ9nxlS5ZlYavN_0,11153
47
+ novel_downloader/core/fetchers/base/browser.py,sha256=6bXhCLc4hxzQgCoEoMspAY1Hn3TT1hjQNIJX0CSNzgw,11927
48
48
  novel_downloader/core/fetchers/base/rate_limiter.py,sha256=zUYH_PjnKfUzJpcbUPtMkwXxIlF0SH-ZTFlbCUrq060,2724
49
49
  novel_downloader/core/fetchers/base/session.py,sha256=Elfpov2cqojunCrLaaSL9ZgWLNVanUDZvgOoIfk5sSc,13251
50
50
  novel_downloader/core/fetchers/biquge/__init__.py,sha256=9EW4eerGeob4QGoDr11A8Mv7xvcfWVFU57M3VT9vzPI,236
@@ -54,7 +54,7 @@ novel_downloader/core/fetchers/common/__init__.py,sha256=ur_zQHrmJdPsFvpyC8AWjsZ
54
54
  novel_downloader/core/fetchers/common/browser.py,sha256=RgNOizfgi_59Xaee6lTlpfiCMGjz_4luNDlHLQqUpl0,2289
55
55
  novel_downloader/core/fetchers/common/session.py,sha256=Ydtwun9lPN4VEIIEIA7Skm6_GOiF_9VDUPkjGQQTdtM,2299
56
56
  novel_downloader/core/fetchers/esjzone/__init__.py,sha256=Cr30WpKEnCrG_vVqttfI9T0zdkwDsLFOnCxQz4EAQQA,242
57
- novel_downloader/core/fetchers/esjzone/browser.py,sha256=SPvbKURw2DkUAlOBLbklmRCFvDoI4tAUDwmBRo2Kuc8,6224
57
+ novel_downloader/core/fetchers/esjzone/browser.py,sha256=bs9o97FRbtx8VSp1AfS0sljD0Izc16JDWqB_EL6GHT4,6278
58
58
  novel_downloader/core/fetchers/esjzone/session.py,sha256=hwvS9LMWm5PYHTTZqYkBgWqkVqkrCfQcaFh6NBICt1Q,7218
59
59
  novel_downloader/core/fetchers/linovelib/__init__.py,sha256=sMNXSBvn8gaZxNX5x4Ork8RzXxL7PhuigquWx6zQ6A4,254
60
60
  novel_downloader/core/fetchers/linovelib/browser.py,sha256=9rQzmJwtu_FHDDQvgDrVIZdwHA7CjnBkIgdyBuy7X50,6086
@@ -63,13 +63,13 @@ novel_downloader/core/fetchers/qianbi/__init__.py,sha256=h4Rve7fO1GcSJ-DlNC5zw7f
63
63
  novel_downloader/core/fetchers/qianbi/browser.py,sha256=1EmrSwpqSYhEO_ID3RJbUaAOhcqvVnMnch6iOafXbTA,3162
64
64
  novel_downloader/core/fetchers/qianbi/session.py,sha256=c3pJcgi9C1x9QYTBihvazHcgT7XTp2HBYfStTn6gSEg,3141
65
65
  novel_downloader/core/fetchers/qidian/__init__.py,sha256=2LshlX82lFpWZMV6yujHsfue9KM0-F1O3HvMCopIv9M,236
66
- novel_downloader/core/fetchers/qidian/browser.py,sha256=cUgJMrFe_MITHh4MGrDI2-a-MCqXSsoeJsbDus0l2LY,10448
67
- novel_downloader/core/fetchers/qidian/session.py,sha256=WrHsov15PoRDYbb1ZRaScJQGRZv6axz9_hVEC1Wt1PM,9746
66
+ novel_downloader/core/fetchers/qidian/browser.py,sha256=0L2iUyOlFNmgKtSb89VMrF7L-MjiJy4E6SSKWxzeSC8,10448
67
+ novel_downloader/core/fetchers/qidian/session.py,sha256=dsawkalQGjbS-E9lcOZMTr9qZz6w__gnmAg-uRUtDKI,9809
68
68
  novel_downloader/core/fetchers/sfacg/__init__.py,sha256=bQAIwERsX9XOKrP2LteFKX8Jlhw4oeUNwpZTHXn5RRg,230
69
69
  novel_downloader/core/fetchers/sfacg/browser.py,sha256=15PVS75PxEKR5W7mQbqVxoN0d4V1XVYVF0l1yy_sv_Y,5681
70
70
  novel_downloader/core/fetchers/sfacg/session.py,sha256=9K4emQCRq45vzYn-ZDX549tK2F92x2CBMp4ODohNOjc,5085
71
71
  novel_downloader/core/fetchers/yamibo/__init__.py,sha256=5ds6DNNvpo6F6U5dboEaIsJoKSPorkPte_HWVnXMdXo,236
72
- novel_downloader/core/fetchers/yamibo/browser.py,sha256=7YRrWbA8_cOcT_z-VjMWP6FUg30TwV6eLW4zJZ_UxSE,7249
72
+ novel_downloader/core/fetchers/yamibo/browser.py,sha256=rkLgIIxLeFV2r4oAT0pOgiKgnp9HJ6X_fwfQIShhgj0,7265
73
73
  novel_downloader/core/fetchers/yamibo/session.py,sha256=434EArdKgEYIBZkb1nMub3PQXdRTS-Ov2_1u9MESjOs,7212
74
74
  novel_downloader/core/interfaces/__init__.py,sha256=hB1SjBzuN7qnZx_h3RV4w_roj3ZwShbIG3CV9jGMB14,602
75
75
  novel_downloader/core/interfaces/downloader.py,sha256=H0S4o5MW22JcvNVECv1bCdwAN0drBwHY31IbvVHn-X0,1557
@@ -90,14 +90,14 @@ novel_downloader/core/parsers/linovelib/main_parser.py,sha256=LqC6W-Lk7sdbiqhYvq
90
90
  novel_downloader/core/parsers/qianbi/__init__.py,sha256=CNmoER8U2u4-ix5S0DDq-pHTtkLR0IZf2SLaTTYXee4,173
91
91
  novel_downloader/core/parsers/qianbi/main_parser.py,sha256=kMjGew_dmqjI9oIThyHgxThZZgS3IaESB_tNf1nkCKk,5069
92
92
  novel_downloader/core/parsers/qidian/__init__.py,sha256=fWWFeyythX0gpDCJ-2AslrRl2hq4vW2bx9hHh1W7mAw,173
93
- novel_downloader/core/parsers/qidian/book_info_parser.py,sha256=h5oAVJOOSZMV4U2GnSezVHPJOEUKUic_oD2gl94mEPQ,3721
94
- novel_downloader/core/parsers/qidian/chapter_encrypted.py,sha256=VhPDERJq1ROJaBlaJuCXJGffisOxkeYFDUKXHo31HfU,18263
95
- novel_downloader/core/parsers/qidian/chapter_normal.py,sha256=6mD86OalpxiCDhjzUjcfFYsgmnTRU_rMIgFN8_Ceu50,4453
93
+ novel_downloader/core/parsers/qidian/book_info_parser.py,sha256=TbP8nghoxzBi435cSJKAzWttBRUq49MHw6kn0klfZrU,3056
94
+ novel_downloader/core/parsers/qidian/chapter_encrypted.py,sha256=uIVmRH96O_G0hcvg2nufUQDrZzq_XRhfvB_8YRW22g8,18532
95
+ novel_downloader/core/parsers/qidian/chapter_normal.py,sha256=CClt6zVph7qjZ0HaiYzMPYlcFgnPIY783-SLVIXNL7E,4738
96
96
  novel_downloader/core/parsers/qidian/chapter_router.py,sha256=foVMlWtE-qUOvJD_4EDiuAVaNkFdeV_ZTCvS5IL7Orc,1957
97
- novel_downloader/core/parsers/qidian/main_parser.py,sha256=SPU6sUO8e4Gp_1q2WypuUQYEyHhoXM7OiRsthhFT6SM,4396
98
- novel_downloader/core/parsers/qidian/utils/__init__.py,sha256=HfjXrGAwH_ceRSE4m88Dl9qb7vHMGcPFxxTU3qrTld8,548
97
+ novel_downloader/core/parsers/qidian/main_parser.py,sha256=jeo7Ypnf-tsBGucjBYBdPzXGd8N4N2eOERuHREloT0o,4504
98
+ novel_downloader/core/parsers/qidian/utils/__init__.py,sha256=-7-nNMtX-KONSN3Q6-f3Yaxt9Oc3Y63kg5ZvNl6xlkY,588
99
99
  novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py,sha256=8ytJnAfiJIxj0wlke9UwYA6vngUyLxVZt8PbfkNUhss,4687
100
- novel_downloader/core/parsers/qidian/utils/helpers.py,sha256=-7vd1BMu5pVFCRySfPWyrItVbZ2wrHPNY8iAQEP7T_8,3264
100
+ novel_downloader/core/parsers/qidian/utils/helpers.py,sha256=v64zTDQ6qbKnGXM7MPzCsS7yQ-w730vPVrDD19Bgx4Q,3514
101
101
  novel_downloader/core/parsers/qidian/utils/node_decryptor.py,sha256=gjqirr5RECScFw0C6DET7ZMaLTcqGcPOmlkwFUJbTHQ,5965
102
102
  novel_downloader/core/parsers/sfacg/__init__.py,sha256=O2nscvtOweMXHMONdvySTsLSy1ulhv53WTp4r6J47tI,169
103
103
  novel_downloader/core/parsers/sfacg/main_parser.py,sha256=yW18MALAZisJdFO-7peI1h-4XVloEDXeCJvXUz5hJ1A,5899
@@ -108,12 +108,12 @@ novel_downloader/locales/zh.json,sha256=7kkKGt1fudSC9LvmFyAgXfyCC512XfCoyf5by8Eg
108
108
  novel_downloader/models/__init__.py,sha256=5aQ24IeU8OJDhBZdZ8Ov-xWoLlZm6Qg24r5FPyLTa6Q,1102
109
109
  novel_downloader/models/browser.py,sha256=ly-jM7izQ77yTIG-oau51HJofDpBfrXpIJZJjoQyad8,435
110
110
  novel_downloader/models/chapter.py,sha256=bdAQUDZIuuTVxoYjoOJrbS2u81b1B2mkuZkTSf0m2HQ,492
111
- novel_downloader/models/config.py,sha256=fYExgkyylDuCUUW8nTFQLBX2rotiumvKV6uMZF1G5Ig,2981
111
+ novel_downloader/models/config.py,sha256=mFrGZALhZzalWn9wtgRdwGnmgwh6xE0dXAaO7mFIrRc,3013
112
112
  novel_downloader/models/login.py,sha256=sY2Jom6PLpA9Z3Uy7plZKhda3Gq7awKOOIIaQ79PpWs,371
113
113
  novel_downloader/models/site_rules.py,sha256=kzDB5F8lf4udAO0WVUrgBOR7ave3jsMBxt7cEoG0bnI,2721
114
114
  novel_downloader/models/tasks.py,sha256=e4DYEXQQQewgQyCCHfc0UYnkPJ96LafmhG3oO5MQP0Q,465
115
115
  novel_downloader/models/types.py,sha256=q1KDuGW0SVxQILKKoPXKRecfaKe2m8jUM0nyaeDJ6dE,394
116
- novel_downloader/resources/config/settings.toml,sha256=A2B_EMh0zjTLi6UBCjhX__igz7xLCXnCqDzJMhiZn8E,4491
116
+ novel_downloader/resources/config/settings.toml,sha256=gp5RrnSHJmnvrx75QwFxadKZVLWh8mnCy99aCwqirdQ,4580
117
117
  novel_downloader/resources/css_styles/main.css,sha256=WM6GePwdOGgM86fbbOxQ0_0oerTBDZeQHt8zRVfcJp8,1617
118
118
  novel_downloader/resources/css_styles/volume-intro.css,sha256=6gaUnNKkrb2w8tYJRq1BGD1FwbhT1I5W2GI_Zelo9G4,1156
119
119
  novel_downloader/resources/images/volume_border.png,sha256=2dEVimnTHKOfLMhi7bhkh_5joWNnrqg8duomLSNOZx4,28613
@@ -125,8 +125,8 @@ novel_downloader/tui/__init__.py,sha256=8RB8tBrPcoBzm1tpQlgZqnOZXrdHmlzMRxuk9wsN
125
125
  novel_downloader/tui/app.py,sha256=ytV1u15nGCRj_ff_GeAL3W1XlU6r5Lh_k3HBcAjPRx0,731
126
126
  novel_downloader/tui/main.py,sha256=MBP8SrwEYTpGQm-V9W_4rKnTeslneORfkzFsU3Xj2yA,256
127
127
  novel_downloader/tui/screens/__init__.py,sha256=QsUM5cUEKm7nluQh9acEt37xRWbkZU3vqIpBduepDCU,203
128
- novel_downloader/tui/screens/home.py,sha256=izxzn9Aru_fzenEKI9sOY1FNhyWky9zgzv7GVUMoW_8,6736
129
- novel_downloader/tui/screens/login.py,sha256=eEVmQFZKQX8mCt_qp96QK8t2pB15FeUrk_oV3jm_Mz4,2224
128
+ novel_downloader/tui/screens/home.py,sha256=nH3pRuCOFcSvNV4AYC6RRkWpphmRgY1bNK36OkSVfCg,6994
129
+ novel_downloader/tui/screens/login.py,sha256=GK8dXCtH5lGu3ilT1Cv2VmFCwHMspDNpzw0logj2lz0,2225
130
130
  novel_downloader/tui/styles/home_layout.tcss,sha256=VNJs339qiwNUuqwwdK6VkYThCdsqlw-2mEoZVCspdrw,974
131
131
  novel_downloader/tui/widgets/richlog_handler.py,sha256=bhFb0E7Z7-dRS07y_vMjxjPeCic9AD59MFPLhef_R5g,572
132
132
  novel_downloader/utils/__init__.py,sha256=4iUXNUzxeAnGmpGWsB4K_jckUYEW0u_LqWp_OM7mtK8,78
@@ -139,7 +139,6 @@ novel_downloader/utils/hash_store.py,sha256=HfzthzcKbHbVaHNpqjaAs2wDeq7iIeY8Mzkt
139
139
  novel_downloader/utils/hash_utils.py,sha256=7eC7WsO_kl25OnRYWzIXbCXsewxvCcvRCzetsf-wbTo,3023
140
140
  novel_downloader/utils/i18n.py,sha256=pdAcSIA5Tp-uPEBwNByHL7C1NayTnpOsl7zFv9p2G1k,1033
141
141
  novel_downloader/utils/logger.py,sha256=9h1iFS8_auiquNgOBd-Q2pbbcnAhAKL39yf3PKadu00,3339
142
- novel_downloader/utils/model_loader.py,sha256=JKgRFrr4HlAW9zuDUBAuuo_Kk_T_g9dWiU8E3zYk0vo,1996
143
142
  novel_downloader/utils/network.py,sha256=W0SVr55MSUjTmMPkUvvkH10SRgFx3GWrCN_fDavfk4A,9143
144
143
  novel_downloader/utils/state.py,sha256=FcNJ85GvBu7uEIjy0QHGr4sXMbHPEMkCjwUKNg5EabI,5132
145
144
  novel_downloader/utils/file_utils/__init__.py,sha256=zvOm2qSEmWd_mRGJceGBZb5MYMSDAlWYjS5MkVQNZgI,1159
@@ -147,19 +146,20 @@ novel_downloader/utils/file_utils/io.py,sha256=AZ3NUe6lifGsYt3iYyXyQ2BO41WV8j013
147
146
  novel_downloader/utils/file_utils/normalize.py,sha256=MrsCq4FqmskKRkHRV_J0z0dmn69OerMum-9sqx2XOGM,2023
148
147
  novel_downloader/utils/file_utils/sanitize.py,sha256=rE-u4vpDL10zH8FT8d9wqwWsz-7dR6PJ-LE45K8VaeE,2112
149
148
  novel_downloader/utils/fontocr/__init__.py,sha256=fe-04om3xxBvFKt5BBCApXCzv-Z0K_AY7lv9IB1jEHM,543
150
- novel_downloader/utils/fontocr/ocr_v1.py,sha256=bwkvqKXUcKsgBCrag8jHuIl8AOM_MbV28ZSQ_aO_X6s,11245
151
- novel_downloader/utils/fontocr/ocr_v2.py,sha256=OJ_egORHa9KwVJX4kEABR25D7RRF0jVCRV4WgHUal7M,27307
152
- novel_downloader/utils/text_utils/__init__.py,sha256=JQtEnJ22K9o_syY5PV4Bf_p4BKZonX5g9-A5eqlxpZs,806
149
+ novel_downloader/utils/fontocr/model_loader.py,sha256=aBPZlwZ-rx2tDsWtN3BnB15vceqNkqTEBX1h2pMZf3E,2012
150
+ novel_downloader/utils/fontocr/ocr_v1.py,sha256=S8GAuDUiVv3mciQvgHEibAhJ2DIow6bHs_ZSYvboC4c,11224
151
+ novel_downloader/utils/fontocr/ocr_v2.py,sha256=XiTHNeRcCw4Q441Tfdox6Sq91dHPfUYg5MecO8qfcB8,27286
152
+ novel_downloader/utils/text_utils/__init__.py,sha256=tAO8oCryMAwQoCCpTKaLJagQ6UK8tJXU55MYomWI-6c,913
153
153
  novel_downloader/utils/text_utils/chapter_formatting.py,sha256=WAAEAcI7zI_uIeARDybZfXDdMvGio3VIkANFrK8-8Os,1378
154
154
  novel_downloader/utils/text_utils/diff_display.py,sha256=aUvjMcYO-1_P8ZYiYbmYbJOByKo2bWoBV_ifRuAqwb8,2528
155
155
  novel_downloader/utils/text_utils/font_mapping.py,sha256=Aos5skBhowDdPgnYmK0bpLtNm2hZg3RolNlTkxC9kO8,865
156
- novel_downloader/utils/text_utils/text_cleaning.py,sha256=_Nahr8iv3341IyDXW-KpTn4XNG2hB-HajSM52pysPu8,1630
156
+ novel_downloader/utils/text_utils/text_cleaning.py,sha256=zufXt0pc0vnUwgSHD_8LAef3ffqmX9rJ11YzW4rgwLA,2940
157
157
  novel_downloader/utils/time_utils/__init__.py,sha256=725vY2PvqFhjbAz0hCOuIuhSCK8HrEqQ_k3YwvubmXo,624
158
- novel_downloader/utils/time_utils/datetime_utils.py,sha256=1eyX8lTEqkQ-rEej_GrhbUpaIR5tsFdVSKT-q8s9X-g,4927
158
+ novel_downloader/utils/time_utils/datetime_utils.py,sha256=3sPdUjDC7Y7dcX6kaQSBu49BU1tnpERNKq0v9lnD170,4932
159
159
  novel_downloader/utils/time_utils/sleep_utils.py,sha256=C4XYeAtxoVZC9Ju6vhhP9sbOrSpdZG2Nm-x1IYO_OFA,3233
160
- novel_downloader-1.4.2.dist-info/licenses/LICENSE,sha256=XgmnH0mBf-qEiizoVAfJQAKzPB9y3rBa-ni7M0Aqv4A,1066
161
- novel_downloader-1.4.2.dist-info/METADATA,sha256=ogSqUunbmTtsABDdShe_XYxGYLPqyvGnmwKyuAJ82bI,7151
162
- novel_downloader-1.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
163
- novel_downloader-1.4.2.dist-info/entry_points.txt,sha256=u1Ns5xI_QJyL4HAFCgJvJdib9ugu7M9I2tnQwZjJxrk,112
164
- novel_downloader-1.4.2.dist-info/top_level.txt,sha256=hP4jYWM2LTm1jxsW4hqEB8N0dsRvldO2QdhggJT917I,17
165
- novel_downloader-1.4.2.dist-info/RECORD,,
160
+ novel_downloader-1.4.4.dist-info/licenses/LICENSE,sha256=XgmnH0mBf-qEiizoVAfJQAKzPB9y3rBa-ni7M0Aqv4A,1066
161
+ novel_downloader-1.4.4.dist-info/METADATA,sha256=JrEV-At77txIIajUrfOix5WYcMVvRkhjf5h601ozbVg,7253
162
+ novel_downloader-1.4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
163
+ novel_downloader-1.4.4.dist-info/entry_points.txt,sha256=u1Ns5xI_QJyL4HAFCgJvJdib9ugu7M9I2tnQwZjJxrk,112
164
+ novel_downloader-1.4.4.dist-info/top_level.txt,sha256=hP4jYWM2LTm1jxsW4hqEB8N0dsRvldO2QdhggJT917I,17
165
+ novel_downloader-1.4.4.dist-info/RECORD,,