novel-downloader 1.4.5__py3-none-any.whl → 1.5.0__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 (165) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +2 -2
  3. novel_downloader/cli/config.py +1 -83
  4. novel_downloader/cli/download.py +4 -5
  5. novel_downloader/cli/export.py +4 -1
  6. novel_downloader/cli/main.py +2 -0
  7. novel_downloader/cli/search.py +123 -0
  8. novel_downloader/config/__init__.py +3 -10
  9. novel_downloader/config/adapter.py +190 -54
  10. novel_downloader/config/loader.py +2 -3
  11. novel_downloader/core/__init__.py +13 -13
  12. novel_downloader/core/downloaders/__init__.py +10 -11
  13. novel_downloader/core/downloaders/base.py +152 -26
  14. novel_downloader/core/downloaders/biquge.py +5 -1
  15. novel_downloader/core/downloaders/common.py +157 -378
  16. novel_downloader/core/downloaders/esjzone.py +5 -1
  17. novel_downloader/core/downloaders/linovelib.py +5 -1
  18. novel_downloader/core/downloaders/qianbi.py +291 -4
  19. novel_downloader/core/downloaders/qidian.py +199 -285
  20. novel_downloader/core/downloaders/registry.py +67 -0
  21. novel_downloader/core/downloaders/sfacg.py +5 -1
  22. novel_downloader/core/downloaders/yamibo.py +5 -1
  23. novel_downloader/core/exporters/__init__.py +10 -11
  24. novel_downloader/core/exporters/base.py +87 -7
  25. novel_downloader/core/exporters/biquge.py +5 -8
  26. novel_downloader/core/exporters/common/__init__.py +2 -2
  27. novel_downloader/core/exporters/common/epub.py +82 -166
  28. novel_downloader/core/exporters/common/main_exporter.py +0 -60
  29. novel_downloader/core/exporters/common/txt.py +82 -83
  30. novel_downloader/core/exporters/epub_util.py +157 -1330
  31. novel_downloader/core/exporters/esjzone.py +5 -8
  32. novel_downloader/core/exporters/linovelib/__init__.py +2 -2
  33. novel_downloader/core/exporters/linovelib/epub.py +157 -212
  34. novel_downloader/core/exporters/linovelib/main_exporter.py +2 -59
  35. novel_downloader/core/exporters/linovelib/txt.py +67 -63
  36. novel_downloader/core/exporters/qianbi.py +5 -8
  37. novel_downloader/core/exporters/qidian.py +14 -4
  38. novel_downloader/core/exporters/registry.py +53 -0
  39. novel_downloader/core/exporters/sfacg.py +5 -8
  40. novel_downloader/core/exporters/txt_util.py +67 -0
  41. novel_downloader/core/exporters/yamibo.py +5 -8
  42. novel_downloader/core/fetchers/__init__.py +19 -24
  43. novel_downloader/core/fetchers/base/__init__.py +3 -3
  44. novel_downloader/core/fetchers/base/browser.py +23 -4
  45. novel_downloader/core/fetchers/base/session.py +30 -5
  46. novel_downloader/core/fetchers/biquge/__init__.py +3 -3
  47. novel_downloader/core/fetchers/biquge/browser.py +5 -0
  48. novel_downloader/core/fetchers/biquge/session.py +6 -1
  49. novel_downloader/core/fetchers/esjzone/__init__.py +3 -3
  50. novel_downloader/core/fetchers/esjzone/browser.py +5 -0
  51. novel_downloader/core/fetchers/esjzone/session.py +6 -1
  52. novel_downloader/core/fetchers/linovelib/__init__.py +3 -3
  53. novel_downloader/core/fetchers/linovelib/browser.py +6 -1
  54. novel_downloader/core/fetchers/linovelib/session.py +6 -1
  55. novel_downloader/core/fetchers/qianbi/__init__.py +3 -3
  56. novel_downloader/core/fetchers/qianbi/browser.py +5 -0
  57. novel_downloader/core/fetchers/qianbi/session.py +5 -0
  58. novel_downloader/core/fetchers/qidian/__init__.py +3 -3
  59. novel_downloader/core/fetchers/qidian/browser.py +12 -4
  60. novel_downloader/core/fetchers/qidian/session.py +11 -3
  61. novel_downloader/core/fetchers/registry.py +71 -0
  62. novel_downloader/core/fetchers/sfacg/__init__.py +3 -3
  63. novel_downloader/core/fetchers/sfacg/browser.py +5 -0
  64. novel_downloader/core/fetchers/sfacg/session.py +5 -0
  65. novel_downloader/core/fetchers/yamibo/__init__.py +3 -3
  66. novel_downloader/core/fetchers/yamibo/browser.py +5 -0
  67. novel_downloader/core/fetchers/yamibo/session.py +6 -1
  68. novel_downloader/core/interfaces/__init__.py +7 -5
  69. novel_downloader/core/interfaces/searcher.py +18 -0
  70. novel_downloader/core/parsers/__init__.py +10 -11
  71. novel_downloader/core/parsers/{biquge/main_parser.py → biquge.py} +7 -2
  72. novel_downloader/core/parsers/{esjzone/main_parser.py → esjzone.py} +7 -2
  73. novel_downloader/core/parsers/{linovelib/main_parser.py → linovelib.py} +7 -2
  74. novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +7 -2
  75. novel_downloader/core/parsers/qidian/__init__.py +2 -2
  76. novel_downloader/core/parsers/qidian/chapter_encrypted.py +23 -21
  77. novel_downloader/core/parsers/qidian/chapter_normal.py +1 -1
  78. novel_downloader/core/parsers/qidian/main_parser.py +10 -21
  79. novel_downloader/core/parsers/qidian/utils/__init__.py +11 -11
  80. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +5 -6
  81. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
  82. novel_downloader/core/parsers/registry.py +68 -0
  83. novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +7 -2
  84. novel_downloader/core/parsers/{yamibo/main_parser.py → yamibo.py} +7 -2
  85. novel_downloader/core/searchers/__init__.py +20 -0
  86. novel_downloader/core/searchers/base.py +92 -0
  87. novel_downloader/core/searchers/biquge.py +83 -0
  88. novel_downloader/core/searchers/esjzone.py +84 -0
  89. novel_downloader/core/searchers/qianbi.py +131 -0
  90. novel_downloader/core/searchers/qidian.py +87 -0
  91. novel_downloader/core/searchers/registry.py +63 -0
  92. novel_downloader/locales/en.json +12 -4
  93. novel_downloader/locales/zh.json +12 -4
  94. novel_downloader/models/__init__.py +4 -30
  95. novel_downloader/models/config.py +12 -6
  96. novel_downloader/models/search.py +16 -0
  97. novel_downloader/models/types.py +0 -2
  98. novel_downloader/resources/config/settings.toml +31 -4
  99. novel_downloader/resources/css_styles/intro.css +83 -0
  100. novel_downloader/resources/css_styles/main.css +30 -89
  101. novel_downloader/utils/__init__.py +52 -0
  102. novel_downloader/utils/chapter_storage.py +244 -224
  103. novel_downloader/utils/constants.py +1 -21
  104. novel_downloader/utils/epub/__init__.py +34 -0
  105. novel_downloader/utils/epub/builder.py +377 -0
  106. novel_downloader/utils/epub/constants.py +77 -0
  107. novel_downloader/utils/epub/documents.py +403 -0
  108. novel_downloader/utils/epub/models.py +134 -0
  109. novel_downloader/utils/epub/utils.py +212 -0
  110. novel_downloader/utils/file_utils/__init__.py +10 -14
  111. novel_downloader/utils/file_utils/io.py +20 -51
  112. novel_downloader/utils/file_utils/normalize.py +2 -2
  113. novel_downloader/utils/file_utils/sanitize.py +2 -3
  114. novel_downloader/utils/fontocr/__init__.py +5 -5
  115. novel_downloader/utils/{hash_store.py → fontocr/hash_store.py} +4 -3
  116. novel_downloader/utils/{hash_utils.py → fontocr/hash_utils.py} +2 -2
  117. novel_downloader/utils/fontocr/ocr_v1.py +13 -1
  118. novel_downloader/utils/fontocr/ocr_v2.py +13 -1
  119. novel_downloader/utils/fontocr/ocr_v3.py +744 -0
  120. novel_downloader/utils/i18n.py +2 -0
  121. novel_downloader/utils/logger.py +2 -0
  122. novel_downloader/utils/network.py +110 -251
  123. novel_downloader/utils/state.py +1 -0
  124. novel_downloader/utils/text_utils/__init__.py +18 -17
  125. novel_downloader/utils/text_utils/diff_display.py +4 -5
  126. novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
  127. novel_downloader/utils/text_utils/text_cleaner.py +179 -0
  128. novel_downloader/utils/text_utils/truncate_utils.py +62 -0
  129. novel_downloader/utils/time_utils/__init__.py +3 -3
  130. novel_downloader/utils/time_utils/datetime_utils.py +4 -5
  131. novel_downloader/utils/time_utils/sleep_utils.py +2 -3
  132. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/METADATA +2 -2
  133. novel_downloader-1.5.0.dist-info/RECORD +164 -0
  134. novel_downloader/config/site_rules.py +0 -94
  135. novel_downloader/core/factory/__init__.py +0 -20
  136. novel_downloader/core/factory/downloader.py +0 -73
  137. novel_downloader/core/factory/exporter.py +0 -58
  138. novel_downloader/core/factory/fetcher.py +0 -96
  139. novel_downloader/core/factory/parser.py +0 -86
  140. novel_downloader/core/fetchers/common/__init__.py +0 -14
  141. novel_downloader/core/fetchers/common/browser.py +0 -79
  142. novel_downloader/core/fetchers/common/session.py +0 -79
  143. novel_downloader/core/parsers/biquge/__init__.py +0 -10
  144. novel_downloader/core/parsers/common/__init__.py +0 -13
  145. novel_downloader/core/parsers/common/helper.py +0 -323
  146. novel_downloader/core/parsers/common/main_parser.py +0 -106
  147. novel_downloader/core/parsers/esjzone/__init__.py +0 -10
  148. novel_downloader/core/parsers/linovelib/__init__.py +0 -10
  149. novel_downloader/core/parsers/qianbi/__init__.py +0 -10
  150. novel_downloader/core/parsers/sfacg/__init__.py +0 -10
  151. novel_downloader/core/parsers/yamibo/__init__.py +0 -10
  152. novel_downloader/models/browser.py +0 -21
  153. novel_downloader/models/site_rules.py +0 -99
  154. novel_downloader/models/tasks.py +0 -33
  155. novel_downloader/resources/css_styles/volume-intro.css +0 -56
  156. novel_downloader/resources/json/replace_word_map.json +0 -4
  157. novel_downloader/resources/text/blacklist.txt +0 -22
  158. novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
  159. novel_downloader/utils/text_utils/font_mapping.py +0 -28
  160. novel_downloader/utils/text_utils/text_cleaning.py +0 -107
  161. novel_downloader-1.4.5.dist-info/RECORD +0 -165
  162. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/WHEEL +0 -0
  163. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/entry_points.txt +0 -0
  164. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/licenses/LICENSE +0 -0
  165. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/top_level.txt +0 -0
@@ -8,9 +8,14 @@ novel_downloader.core.fetchers.biquge.browser
8
8
  from typing import Any
9
9
 
10
10
  from novel_downloader.core.fetchers.base import BaseBrowser
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
11
12
  from novel_downloader.models import FetcherConfig
12
13
 
13
14
 
15
+ @register_fetcher(
16
+ site_keys=["biquge", "bqg"],
17
+ backends=["browser"],
18
+ )
14
19
  class BiqugeBrowser(BaseBrowser):
15
20
  """
16
21
  A browser class for interacting with the Biquge (www.b520.cc) novel website.
@@ -8,9 +8,14 @@ novel_downloader.core.fetchers.biquge.session
8
8
  from typing import Any
9
9
 
10
10
  from novel_downloader.core.fetchers.base import BaseSession
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
11
12
  from novel_downloader.models import FetcherConfig
12
13
 
13
14
 
15
+ @register_fetcher(
16
+ site_keys=["biquge", "bqg"],
17
+ backends=["session"],
18
+ )
14
19
  class BiqugeSession(BaseSession):
15
20
  """
16
21
  A session class for interacting with the Biquge (www.b520.cc) novel website.
@@ -55,7 +60,7 @@ class BiqugeSession(BaseSession):
55
60
  :return: The chapter content as a string.
56
61
  """
57
62
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
58
- return [await self.fetch(url, **kwargs)]
63
+ return [await self.fetch(url, encoding="gbk", **kwargs)]
59
64
 
60
65
  @classmethod
61
66
  def book_info_url(cls, book_id: str) -> str:
@@ -5,10 +5,10 @@ novel_downloader.core.fetchers.esjzone
5
5
 
6
6
  """
7
7
 
8
- from .browser import EsjzoneBrowser
9
- from .session import EsjzoneSession
10
-
11
8
  __all__ = [
12
9
  "EsjzoneBrowser",
13
10
  "EsjzoneSession",
14
11
  ]
12
+
13
+ from .browser import EsjzoneBrowser
14
+ from .session import EsjzoneSession
@@ -8,9 +8,14 @@ novel_downloader.core.fetchers.esjzone.browser
8
8
  from typing import Any
9
9
 
10
10
  from novel_downloader.core.fetchers.base import BaseBrowser
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
11
12
  from novel_downloader.models import FetcherConfig, LoginField
12
13
 
13
14
 
15
+ @register_fetcher(
16
+ site_keys=["esjzone"],
17
+ backends=["browser"],
18
+ )
14
19
  class EsjzoneBrowser(BaseBrowser):
15
20
  """
16
21
  A browser class for interacting with the Esjzone (www.esjzone.cc) novel website.
@@ -9,10 +9,15 @@ import re
9
9
  from typing import Any
10
10
 
11
11
  from novel_downloader.core.fetchers.base import BaseSession
12
+ from novel_downloader.core.fetchers.registry import register_fetcher
12
13
  from novel_downloader.models import FetcherConfig, LoginField
13
- from novel_downloader.utils.time_utils import async_sleep_with_random_delay
14
+ from novel_downloader.utils import async_sleep_with_random_delay
14
15
 
15
16
 
17
+ @register_fetcher(
18
+ site_keys=["esjzone"],
19
+ backends=["session"],
20
+ )
16
21
  class EsjzoneSession(BaseSession):
17
22
  """
18
23
  A session class for interacting with the esjzone (www.esjzone.cc) novel website.
@@ -5,10 +5,10 @@ novel_downloader.core.fetchers.linovelib
5
5
 
6
6
  """
7
7
 
8
- from .browser import LinovelibBrowser
9
- from .session import LinovelibSession
10
-
11
8
  __all__ = [
12
9
  "LinovelibBrowser",
13
10
  "LinovelibSession",
14
11
  ]
12
+
13
+ from .browser import LinovelibBrowser
14
+ from .session import LinovelibSession
@@ -9,10 +9,15 @@ import re
9
9
  from typing import Any
10
10
 
11
11
  from novel_downloader.core.fetchers.base import BaseBrowser
12
+ from novel_downloader.core.fetchers.registry import register_fetcher
12
13
  from novel_downloader.models import FetcherConfig
13
- from novel_downloader.utils.time_utils import async_sleep_with_random_delay
14
+ from novel_downloader.utils import async_sleep_with_random_delay
14
15
 
15
16
 
17
+ @register_fetcher(
18
+ site_keys=["linovelib"],
19
+ backends=["browser"],
20
+ )
16
21
  class LinovelibBrowser(BaseBrowser):
17
22
  """
18
23
  A browser class for interacting with Linovelib (www.linovelib.com) novel website.
@@ -9,10 +9,15 @@ import re
9
9
  from typing import Any
10
10
 
11
11
  from novel_downloader.core.fetchers.base import BaseSession
12
+ from novel_downloader.core.fetchers.registry import register_fetcher
12
13
  from novel_downloader.models import FetcherConfig
13
- from novel_downloader.utils.time_utils import async_sleep_with_random_delay
14
+ from novel_downloader.utils import async_sleep_with_random_delay
14
15
 
15
16
 
17
+ @register_fetcher(
18
+ site_keys=["linovelib"],
19
+ backends=["session"],
20
+ )
16
21
  class LinovelibSession(BaseSession):
17
22
  """
18
23
  A session class for interacting with Linovelib (www.linovelib.com) novel website.
@@ -5,10 +5,10 @@ novel_downloader.core.fetchers.qianbi
5
5
 
6
6
  """
7
7
 
8
- from .browser import QianbiBrowser
9
- from .session import QianbiSession
10
-
11
8
  __all__ = [
12
9
  "QianbiBrowser",
13
10
  "QianbiSession",
14
11
  ]
12
+
13
+ from .browser import QianbiBrowser
14
+ from .session import QianbiSession
@@ -8,9 +8,14 @@ novel_downloader.core.fetchers.qianbi.browser
8
8
  from typing import Any
9
9
 
10
10
  from novel_downloader.core.fetchers.base import BaseBrowser
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
11
12
  from novel_downloader.models import FetcherConfig
12
13
 
13
14
 
15
+ @register_fetcher(
16
+ site_keys=["qianbi"],
17
+ backends=["browser"],
18
+ )
14
19
  class QianbiBrowser(BaseBrowser):
15
20
  """
16
21
  A browser class for interacting with the Qianbi (www.23qb.com) novel website.
@@ -9,9 +9,14 @@ import asyncio
9
9
  from typing import Any
10
10
 
11
11
  from novel_downloader.core.fetchers.base import BaseSession
12
+ from novel_downloader.core.fetchers.registry import register_fetcher
12
13
  from novel_downloader.models import FetcherConfig
13
14
 
14
15
 
16
+ @register_fetcher(
17
+ site_keys=["qianbi"],
18
+ backends=["session"],
19
+ )
15
20
  class QianbiSession(BaseSession):
16
21
  """
17
22
  A session class for interacting with the Qianbi (www.23qb.com) novel website.
@@ -5,10 +5,10 @@ novel_downloader.core.fetchers.qidian
5
5
 
6
6
  """
7
7
 
8
- from .browser import QidianBrowser
9
- from .session import QidianSession
10
-
11
8
  __all__ = [
12
9
  "QidianBrowser",
13
10
  "QidianSession",
14
11
  ]
12
+
13
+ from .browser import QidianBrowser
14
+ from .session import QidianSession
@@ -11,15 +11,21 @@ from typing import Any
11
11
  from playwright.async_api import Page
12
12
 
13
13
  from novel_downloader.core.fetchers.base import BaseBrowser
14
+ from novel_downloader.core.fetchers.registry import register_fetcher
14
15
  from novel_downloader.models import FetcherConfig, LoginField
15
16
  from novel_downloader.utils.i18n import t
16
17
 
17
18
 
19
+ @register_fetcher(
20
+ site_keys=["qidian", "qd"],
21
+ backends=["browser"],
22
+ )
18
23
  class QidianBrowser(BaseBrowser):
19
24
  """
20
25
  A browser class for interacting with the Qidian (www.qidian.com) novel website.
21
26
  """
22
27
 
28
+ WAIT_TIME = 2.0
23
29
  HOMEPAGE_URL = "https://www.qidian.com/"
24
30
  BOOKCASE_URL = "https://my.qidian.com/bookcase/"
25
31
  # BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
@@ -59,7 +65,7 @@ class QidianBrowser(BaseBrowser):
59
65
  :return: The page content as a string.
60
66
  """
61
67
  url = self.book_info_url(book_id=book_id)
62
- return [await self.fetch(url, **kwargs)]
68
+ return [await self.fetch(url, delay=self.WAIT_TIME, **kwargs)]
63
69
 
64
70
  async def get_book_chapter(
65
71
  self,
@@ -76,7 +82,9 @@ class QidianBrowser(BaseBrowser):
76
82
  """
77
83
  catalog_url = self.book_info_url(book_id=book_id)
78
84
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
79
- return [await self.fetch(url, referer=catalog_url, **kwargs)]
85
+ return [
86
+ await self.fetch(url, referer=catalog_url, delay=self.WAIT_TIME, **kwargs)
87
+ ]
80
88
 
81
89
  async def get_bookcase(
82
90
  self,
@@ -88,7 +96,7 @@ class QidianBrowser(BaseBrowser):
88
96
  :return: The HTML markup of the bookcase page.
89
97
  """
90
98
  url = self.bookcase_url()
91
- return [await self.fetch(url, **kwargs)]
99
+ return [await self.fetch(url, delay=self.WAIT_TIME, **kwargs)]
92
100
 
93
101
  async def get_homepage(
94
102
  self,
@@ -100,7 +108,7 @@ class QidianBrowser(BaseBrowser):
100
108
  :return: The HTML markup of the home page.
101
109
  """
102
110
  url = self.homepage_url()
103
- return [await self.fetch(url, **kwargs)]
111
+ return [await self.fetch(url, delay=self.WAIT_TIME, **kwargs)]
104
112
 
105
113
  async def set_interactive_mode(self, enable: bool) -> bool:
106
114
  """
@@ -15,11 +15,18 @@ from typing import Any, ClassVar
15
15
  import aiohttp
16
16
 
17
17
  from novel_downloader.core.fetchers.base import BaseSession
18
+ from novel_downloader.core.fetchers.registry import register_fetcher
18
19
  from novel_downloader.models import FetcherConfig, LoginField
19
- from novel_downloader.utils.crypto_utils import rc4_crypt
20
- from novel_downloader.utils.time_utils import async_sleep_with_random_delay
20
+ from novel_downloader.utils import (
21
+ async_sleep_with_random_delay,
22
+ rc4_crypt,
23
+ )
21
24
 
22
25
 
26
+ @register_fetcher(
27
+ site_keys=["qidian", "qd"],
28
+ backends=["session"],
29
+ )
23
30
  class QidianSession(BaseSession):
24
31
  """
25
32
  A session class for interacting with the Qidian (www.qidian.com) novel website.
@@ -144,6 +151,7 @@ class QidianSession(BaseSession):
144
151
  async def fetch(
145
152
  self,
146
153
  url: str,
154
+ encoding: str | None = None,
147
155
  **kwargs: Any,
148
156
  ) -> str:
149
157
  """
@@ -167,7 +175,7 @@ class QidianSession(BaseSession):
167
175
 
168
176
  async with self.session.get(url, **kwargs) as resp:
169
177
  resp.raise_for_status()
170
- text: str = await resp.text()
178
+ text: str = await resp.text(encoding=encoding)
171
179
  return text
172
180
  except aiohttp.ClientError:
173
181
  if attempt < self.retry_times:
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.registry
4
+ ---------------------------------------
5
+
6
+ """
7
+
8
+ __all__ = ["register_fetcher", "get_fetcher"]
9
+
10
+ from collections.abc import Callable, Sequence
11
+ from typing import TypeVar
12
+
13
+ from novel_downloader.core.interfaces import FetcherProtocol
14
+ from novel_downloader.models import FetcherConfig
15
+
16
+ FetcherBuilder = Callable[[FetcherConfig], FetcherProtocol]
17
+
18
+ F = TypeVar("F", bound=FetcherProtocol)
19
+ _FETCHER_MAP: dict[str, dict[str, FetcherBuilder]] = {}
20
+
21
+
22
+ def register_fetcher(
23
+ site_keys: Sequence[str],
24
+ backends: Sequence[str],
25
+ ) -> Callable[[type[F]], type[F]]:
26
+ """
27
+ Decorator to register a fetcher class under given keys.
28
+
29
+ :param site_keys: Sequence of site identifiers
30
+ :param backends: Sequence of backend types
31
+ :return: A class decorator that populates _FETCHER_MAP.
32
+ """
33
+
34
+ def decorator(cls: type[F]) -> type[F]:
35
+ for site in site_keys:
36
+ site_lower = site.lower()
37
+ bucket = _FETCHER_MAP.setdefault(site_lower, {})
38
+ for backend in backends:
39
+ bucket[backend] = cls
40
+ return cls
41
+
42
+ return decorator
43
+
44
+
45
+ def get_fetcher(
46
+ site: str,
47
+ config: FetcherConfig,
48
+ ) -> FetcherProtocol:
49
+ """
50
+ Returns an FetcherProtocol for the given site.
51
+
52
+ :param site: Site name (e.g., 'qidian')
53
+ :param config: Configuration for the requester
54
+ :return: An instance of a requester class
55
+ """
56
+ site_key = site.lower()
57
+ try:
58
+ backend_map = _FETCHER_MAP[site_key]
59
+ except KeyError as err:
60
+ raise ValueError(f"Unsupported site: {site!r}") from err
61
+
62
+ mode = config.mode
63
+ try:
64
+ fetcher_cls = backend_map[mode]
65
+ except KeyError as err:
66
+ raise ValueError(
67
+ f"Unsupported fetcher mode {mode!r} for site {site!r}. "
68
+ f"Available modes: {list(backend_map)}"
69
+ ) from err
70
+
71
+ return fetcher_cls(config)
@@ -5,10 +5,10 @@ novel_downloader.core.fetchers.sfacg
5
5
 
6
6
  """
7
7
 
8
- from .browser import SfacgBrowser
9
- from .session import SfacgSession
10
-
11
8
  __all__ = [
12
9
  "SfacgBrowser",
13
10
  "SfacgSession",
14
11
  ]
12
+
13
+ from .browser import SfacgBrowser
14
+ from .session import SfacgSession
@@ -8,10 +8,15 @@ novel_downloader.core.fetchers.sfacg.browser
8
8
  from typing import Any
9
9
 
10
10
  from novel_downloader.core.fetchers.base import BaseBrowser
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
11
12
  from novel_downloader.models import FetcherConfig, LoginField
12
13
  from novel_downloader.utils.i18n import t
13
14
 
14
15
 
16
+ @register_fetcher(
17
+ site_keys=["sfacg"],
18
+ backends=["browser"],
19
+ )
15
20
  class SfacgBrowser(BaseBrowser):
16
21
  """
17
22
  A browser class for interacting with the Sfacg (m.sfacg.com) novel website.
@@ -8,9 +8,14 @@ novel_downloader.core.fetchers.sfacg.session
8
8
  from typing import Any
9
9
 
10
10
  from novel_downloader.core.fetchers.base import BaseSession
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
11
12
  from novel_downloader.models import FetcherConfig, LoginField
12
13
 
13
14
 
15
+ @register_fetcher(
16
+ site_keys=["sfacg"],
17
+ backends=["session"],
18
+ )
14
19
  class SfacgSession(BaseSession):
15
20
  """
16
21
  A session class for interacting with the Sfacg (m.sfacg.com) novel website.
@@ -5,10 +5,10 @@ novel_downloader.core.fetchers.yamibo
5
5
 
6
6
  """
7
7
 
8
- from .browser import YamiboBrowser
9
- from .session import YamiboSession
10
-
11
8
  __all__ = [
12
9
  "YamiboBrowser",
13
10
  "YamiboSession",
14
11
  ]
12
+
13
+ from .browser import YamiboBrowser
14
+ from .session import YamiboSession
@@ -8,9 +8,14 @@ novel_downloader.core.fetchers.yamibo.browser
8
8
  from typing import Any
9
9
 
10
10
  from novel_downloader.core.fetchers.base import BaseBrowser
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
11
12
  from novel_downloader.models import FetcherConfig, LoginField
12
13
 
13
14
 
15
+ @register_fetcher(
16
+ site_keys=["yamibo"],
17
+ backends=["browser"],
18
+ )
14
19
  class YamiboBrowser(BaseBrowser):
15
20
  """
16
21
  A browser class for interacting with the Yamibo (www.yamibo.com) novel website.
@@ -10,10 +10,15 @@ from typing import Any
10
10
  from lxml import html
11
11
 
12
12
  from novel_downloader.core.fetchers.base import BaseSession
13
+ from novel_downloader.core.fetchers.registry import register_fetcher
13
14
  from novel_downloader.models import FetcherConfig, LoginField
14
- from novel_downloader.utils.time_utils import async_sleep_with_random_delay
15
+ from novel_downloader.utils import async_sleep_with_random_delay
15
16
 
16
17
 
18
+ @register_fetcher(
19
+ site_keys=["yamibo"],
20
+ backends=["session"],
21
+ )
17
22
  class YamiboSession(BaseSession):
18
23
  """
19
24
  A session class for interacting with the Yamibo (www.yamibo.com) novel website.
@@ -14,14 +14,16 @@ Included protocols:
14
14
  - ExporterProtocol
15
15
  """
16
16
 
17
- from .downloader import DownloaderProtocol
18
- from .exporter import ExporterProtocol
19
- from .fetcher import FetcherProtocol
20
- from .parser import ParserProtocol
21
-
22
17
  __all__ = [
23
18
  "DownloaderProtocol",
24
19
  "ExporterProtocol",
25
20
  "FetcherProtocol",
26
21
  "ParserProtocol",
22
+ "SearcherProtocol",
27
23
  ]
24
+
25
+ from .downloader import DownloaderProtocol
26
+ from .exporter import ExporterProtocol
27
+ from .fetcher import FetcherProtocol
28
+ from .parser import ParserProtocol
29
+ from .searcher import SearcherProtocol
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.interfaces.searcher
4
+ -----------------------------------------
5
+
6
+ """
7
+
8
+ from typing import Protocol
9
+
10
+ from novel_downloader.models import SearchResult
11
+
12
+
13
+ class SearcherProtocol(Protocol):
14
+ site_name: str
15
+
16
+ @classmethod
17
+ def search(cls, keyword: str, limit: int | None = None) -> list[SearchResult]:
18
+ ...
@@ -14,21 +14,11 @@ Modules:
14
14
  - qidian (起点中文网)
15
15
  - sfacg (SF轻小说)
16
16
  - yamibo (百合会)
17
- - common (通用架构)
18
17
  """
19
18
 
20
- from .biquge import BiqugeParser
21
- from .common import CommonParser
22
- from .esjzone import EsjzoneParser
23
- from .linovelib import LinovelibParser
24
- from .qianbi import QianbiParser
25
- from .qidian import QidianParser
26
- from .sfacg import SfacgParser
27
- from .yamibo import YamiboParser
28
-
29
19
  __all__ = [
20
+ "get_parser",
30
21
  "BiqugeParser",
31
- "CommonParser",
32
22
  "EsjzoneParser",
33
23
  "LinovelibParser",
34
24
  "QianbiParser",
@@ -36,3 +26,12 @@ __all__ = [
36
26
  "SfacgParser",
37
27
  "YamiboParser",
38
28
  ]
29
+
30
+ from .biquge import BiqugeParser
31
+ from .esjzone import EsjzoneParser
32
+ from .linovelib import LinovelibParser
33
+ from .qianbi import QianbiParser
34
+ from .qidian import QidianParser
35
+ from .registry import get_parser
36
+ from .sfacg import SfacgParser
37
+ from .yamibo import YamiboParser
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.parsers.biquge.main_parser
4
- ------------------------------------------------
3
+ novel_downloader.core.parsers.biquge
4
+ ------------------------------------
5
5
 
6
6
  """
7
7
 
@@ -11,9 +11,14 @@ from typing import Any
11
11
  from lxml import html
12
12
 
13
13
  from novel_downloader.core.parsers.base import BaseParser
14
+ from novel_downloader.core.parsers.registry import register_parser
14
15
  from novel_downloader.models import ChapterDict
15
16
 
16
17
 
18
+ @register_parser(
19
+ site_keys=["biquge", "bqg"],
20
+ backends=["session", "browser"],
21
+ )
17
22
  class BiqugeParser(BaseParser):
18
23
  """ """
19
24
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.parsers.esjzone.main_parser
4
- -------------------------------------------------
3
+ novel_downloader.core.parsers.esjzone
4
+ -------------------------------------
5
5
 
6
6
  """
7
7
 
@@ -11,9 +11,14 @@ from typing import Any
11
11
  from lxml import html
12
12
 
13
13
  from novel_downloader.core.parsers.base import BaseParser
14
+ from novel_downloader.core.parsers.registry import register_parser
14
15
  from novel_downloader.models import ChapterDict
15
16
 
16
17
 
18
+ @register_parser(
19
+ site_keys=["esjzone"],
20
+ backends=["session", "browser"],
21
+ )
17
22
  class EsjzoneParser(BaseParser):
18
23
  """ """
19
24
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.parsers.linovelib.main_parser
4
- ---------------------------------------------------
3
+ novel_downloader.core.parsers.linovelib
4
+ ---------------------------------------
5
5
 
6
6
  """
7
7
 
@@ -13,10 +13,15 @@ from typing import Any
13
13
  from lxml import html
14
14
 
15
15
  from novel_downloader.core.parsers.base import BaseParser
16
+ from novel_downloader.core.parsers.registry import register_parser
16
17
  from novel_downloader.models import ChapterDict
17
18
  from novel_downloader.utils.constants import LINOVELIB_FONT_MAP_PATH
18
19
 
19
20
 
21
+ @register_parser(
22
+ site_keys=["linovelib"],
23
+ backends=["session", "browser"],
24
+ )
20
25
  class LinovelibParser(BaseParser):
21
26
  """ """
22
27
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.parsers.qianbi.main_parser
4
- ------------------------------------------------
3
+ novel_downloader.core.parsers.qianbi
4
+ ------------------------------------
5
5
 
6
6
  """
7
7
 
@@ -11,9 +11,14 @@ from typing import Any
11
11
  from lxml import html
12
12
 
13
13
  from novel_downloader.core.parsers.base import BaseParser
14
+ from novel_downloader.core.parsers.registry import register_parser
14
15
  from novel_downloader.models import ChapterDict
15
16
 
16
17
 
18
+ @register_parser(
19
+ site_keys=["qianbi"],
20
+ backends=["session", "browser"],
21
+ )
17
22
  class QianbiParser(BaseParser):
18
23
  """ """
19
24
 
@@ -5,6 +5,6 @@ novel_downloader.core.parsers.qidian
5
5
 
6
6
  """
7
7
 
8
- from .main_parser import QidianParser
9
-
10
8
  __all__ = ["QidianParser"]
9
+
10
+ from .main_parser import QidianParser