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.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/download.py +1 -1
- novel_downloader/config/adapter.py +1 -0
- novel_downloader/core/__init__.py +19 -1
- novel_downloader/core/downloaders/base.py +0 -7
- novel_downloader/core/downloaders/biquge.py +1 -3
- novel_downloader/core/downloaders/common.py +0 -2
- novel_downloader/core/downloaders/esjzone.py +1 -3
- novel_downloader/core/downloaders/linovelib.py +1 -3
- novel_downloader/core/downloaders/qianbi.py +1 -3
- novel_downloader/core/downloaders/qidian.py +1 -5
- novel_downloader/core/downloaders/sfacg.py +1 -3
- novel_downloader/core/downloaders/yamibo.py +1 -3
- novel_downloader/core/factory/downloader.py +3 -6
- novel_downloader/core/fetchers/base/browser.py +32 -12
- novel_downloader/core/fetchers/esjzone/browser.py +8 -6
- novel_downloader/core/fetchers/qidian/browser.py +2 -2
- novel_downloader/core/fetchers/qidian/session.py +2 -1
- novel_downloader/core/fetchers/yamibo/browser.py +3 -3
- novel_downloader/core/parsers/qidian/book_info_parser.py +31 -54
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +11 -2
- novel_downloader/core/parsers/qidian/chapter_normal.py +8 -1
- novel_downloader/core/parsers/qidian/main_parser.py +7 -2
- novel_downloader/core/parsers/qidian/utils/__init__.py +2 -0
- novel_downloader/core/parsers/qidian/utils/helpers.py +9 -0
- novel_downloader/models/config.py +1 -0
- novel_downloader/resources/config/settings.toml +1 -0
- novel_downloader/tui/screens/home.py +8 -2
- novel_downloader/tui/screens/login.py +1 -1
- novel_downloader/utils/{model_loader.py → fontocr/model_loader.py} +2 -2
- novel_downloader/utils/fontocr/ocr_v1.py +2 -1
- novel_downloader/utils/fontocr/ocr_v2.py +2 -1
- novel_downloader/utils/text_utils/__init__.py +8 -1
- novel_downloader/utils/text_utils/text_cleaning.py +51 -0
- novel_downloader/utils/time_utils/datetime_utils.py +1 -1
- {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/METADATA +2 -1
- {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/RECORD +41 -41
- {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/WHEEL +0 -0
- {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/entry_points.txt +0 -0
- {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.4.2.dist-info → novel_downloader-1.4.4.dist-info}/top_level.txt +0 -0
novel_downloader/__init__.py
CHANGED
novel_downloader/cli/download.py
CHANGED
@@ -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
|
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,
|
25
|
+
super().__init__(fetcher, parser, config, "biquge")
|
@@ -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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
52
|
+
try:
|
53
|
+
await login_page.goto(self.API_LOGIN_URL_1, wait_until="networkidle")
|
53
54
|
|
54
|
-
|
55
|
-
|
55
|
+
await login_page.fill('input[name="email"]', username)
|
56
|
+
await login_page.fill('input[name="pwd"]', password)
|
56
57
|
|
57
|
-
|
58
|
+
await login_page.click('a.btn-send[data-send="mem_login"]')
|
58
59
|
|
59
|
-
|
60
|
-
|
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
|
-
|
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
|
56
|
-
info["book_name"] = book_name
|
38
|
+
info["book_name"] = doc.xpath('string(//h1[@id="bookName"])').strip()
|
57
39
|
|
58
|
-
author = doc.xpath(
|
59
|
-
info["author"] = author
|
40
|
+
info["author"] = doc.xpath('string(//a[@class="writer-name"])').strip()
|
60
41
|
|
61
|
-
|
62
|
-
info[
|
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
|
-
|
65
|
-
doc.xpath('string(//span[
|
66
|
-
.replace("
|
47
|
+
ut = (
|
48
|
+
doc.xpath('string(//span[@class="update-time"])')
|
49
|
+
.replace("更新时间:", "")
|
67
50
|
.strip()
|
68
51
|
)
|
69
|
-
|
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
|
-
|
72
|
-
|
57
|
+
info["serial_status"] = doc.xpath(
|
58
|
+
'string(//p[@class="book-attribute"]/span[1])'
|
59
|
+
).strip()
|
73
60
|
|
74
|
-
tags = doc.xpath('//p[@class
|
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
|
-
|
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
|
-
|
87
|
-
|
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
|
92
|
-
|
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
|
95
|
-
a = li.xpath(
|
96
|
-
|
97
|
-
|
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":
|
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
|
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("
|
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("
|
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__(
|
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.
|
@@ -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()
|
@@ -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
|
-
|
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
|
-
|
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
|
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
|
]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: novel-downloader
|
3
|
-
Version: 1.4.
|
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=
|
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=
|
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=
|
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=
|
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=
|
15
|
-
novel_downloader/core/downloaders/biquge.py,sha256=
|
16
|
-
novel_downloader/core/downloaders/common.py,sha256
|
17
|
-
novel_downloader/core/downloaders/esjzone.py,sha256=
|
18
|
-
novel_downloader/core/downloaders/linovelib.py,sha256=
|
19
|
-
novel_downloader/core/downloaders/qianbi.py,sha256=
|
20
|
-
novel_downloader/core/downloaders/qidian.py,sha256=
|
21
|
-
novel_downloader/core/downloaders/sfacg.py,sha256=
|
22
|
-
novel_downloader/core/downloaders/yamibo.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
67
|
-
novel_downloader/core/fetchers/qidian/session.py,sha256=
|
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=
|
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=
|
94
|
-
novel_downloader/core/parsers/qidian/chapter_encrypted.py,sha256=
|
95
|
-
novel_downloader/core/parsers/qidian/chapter_normal.py,sha256=
|
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=
|
98
|
-
novel_downloader/core/parsers/qidian/utils/__init__.py,sha256
|
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
|
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=
|
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=
|
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=
|
129
|
-
novel_downloader/tui/screens/login.py,sha256=
|
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/
|
151
|
-
novel_downloader/utils/fontocr/
|
152
|
-
novel_downloader/utils/
|
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=
|
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=
|
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.
|
161
|
-
novel_downloader-1.4.
|
162
|
-
novel_downloader-1.4.
|
163
|
-
novel_downloader-1.4.
|
164
|
-
novel_downloader-1.4.
|
165
|
-
novel_downloader-1.4.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|