novel-downloader 2.0.0__py3-none-any.whl → 2.0.2__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 +14 -11
- novel_downloader/cli/export.py +19 -19
- novel_downloader/cli/ui.py +35 -8
- novel_downloader/config/adapter.py +216 -153
- novel_downloader/core/__init__.py +5 -6
- novel_downloader/core/archived/deqixs/fetcher.py +1 -28
- novel_downloader/core/downloaders/__init__.py +2 -0
- novel_downloader/core/downloaders/base.py +34 -85
- novel_downloader/core/downloaders/common.py +147 -171
- novel_downloader/core/downloaders/qianbi.py +30 -64
- novel_downloader/core/downloaders/qidian.py +157 -184
- novel_downloader/core/downloaders/qqbook.py +292 -0
- novel_downloader/core/downloaders/registry.py +2 -2
- novel_downloader/core/exporters/__init__.py +2 -0
- novel_downloader/core/exporters/base.py +37 -59
- novel_downloader/core/exporters/common.py +620 -0
- novel_downloader/core/exporters/linovelib.py +47 -0
- novel_downloader/core/exporters/qidian.py +41 -12
- novel_downloader/core/exporters/qqbook.py +28 -0
- novel_downloader/core/exporters/registry.py +2 -2
- novel_downloader/core/fetchers/__init__.py +4 -2
- novel_downloader/core/fetchers/aaatxt.py +2 -22
- novel_downloader/core/fetchers/b520.py +3 -23
- novel_downloader/core/fetchers/base.py +80 -105
- novel_downloader/core/fetchers/biquyuedu.py +2 -22
- novel_downloader/core/fetchers/dxmwx.py +10 -22
- novel_downloader/core/fetchers/esjzone.py +6 -29
- novel_downloader/core/fetchers/guidaye.py +2 -22
- novel_downloader/core/fetchers/hetushu.py +9 -29
- novel_downloader/core/fetchers/i25zw.py +2 -16
- novel_downloader/core/fetchers/ixdzs8.py +2 -16
- novel_downloader/core/fetchers/jpxs123.py +2 -16
- novel_downloader/core/fetchers/lewenn.py +2 -22
- novel_downloader/core/fetchers/linovelib.py +4 -20
- novel_downloader/core/fetchers/{eightnovel.py → n8novel.py} +12 -40
- novel_downloader/core/fetchers/piaotia.py +2 -16
- novel_downloader/core/fetchers/qbtr.py +2 -16
- novel_downloader/core/fetchers/qianbi.py +1 -20
- novel_downloader/core/fetchers/qidian.py +27 -68
- novel_downloader/core/fetchers/qqbook.py +177 -0
- novel_downloader/core/fetchers/quanben5.py +9 -29
- novel_downloader/core/fetchers/rate_limiter.py +22 -53
- novel_downloader/core/fetchers/sfacg.py +3 -16
- novel_downloader/core/fetchers/shencou.py +2 -16
- novel_downloader/core/fetchers/shuhaige.py +2 -22
- novel_downloader/core/fetchers/tongrenquan.py +2 -22
- novel_downloader/core/fetchers/ttkan.py +3 -14
- novel_downloader/core/fetchers/wanbengo.py +2 -22
- novel_downloader/core/fetchers/xiaoshuowu.py +2 -16
- novel_downloader/core/fetchers/xiguashuwu.py +4 -20
- novel_downloader/core/fetchers/xs63b.py +3 -15
- novel_downloader/core/fetchers/xshbook.py +2 -22
- novel_downloader/core/fetchers/yamibo.py +4 -28
- novel_downloader/core/fetchers/yibige.py +13 -26
- novel_downloader/core/interfaces/exporter.py +19 -7
- novel_downloader/core/interfaces/fetcher.py +23 -49
- novel_downloader/core/interfaces/parser.py +2 -2
- novel_downloader/core/parsers/__init__.py +4 -2
- novel_downloader/core/parsers/b520.py +2 -2
- novel_downloader/core/parsers/base.py +5 -39
- novel_downloader/core/parsers/esjzone.py +3 -3
- novel_downloader/core/parsers/{eightnovel.py → n8novel.py} +7 -7
- novel_downloader/core/parsers/qidian.py +717 -0
- novel_downloader/core/parsers/qqbook.py +709 -0
- novel_downloader/core/parsers/xiguashuwu.py +8 -15
- novel_downloader/core/searchers/__init__.py +2 -2
- novel_downloader/core/searchers/b520.py +1 -1
- novel_downloader/core/searchers/base.py +2 -2
- novel_downloader/core/searchers/{eightnovel.py → n8novel.py} +5 -5
- novel_downloader/locales/en.json +3 -3
- novel_downloader/locales/zh.json +3 -3
- novel_downloader/models/__init__.py +2 -0
- novel_downloader/models/book.py +1 -0
- novel_downloader/models/config.py +12 -0
- novel_downloader/resources/config/settings.toml +23 -5
- novel_downloader/resources/js_scripts/expr_to_json.js +14 -0
- novel_downloader/resources/js_scripts/qidian_decrypt_node.js +21 -16
- novel_downloader/resources/js_scripts/qq_decrypt_node.js +92 -0
- novel_downloader/utils/__init__.py +0 -2
- novel_downloader/utils/chapter_storage.py +2 -3
- novel_downloader/utils/constants.py +7 -3
- novel_downloader/utils/cookies.py +32 -17
- novel_downloader/utils/crypto_utils/__init__.py +0 -6
- novel_downloader/utils/crypto_utils/aes_util.py +1 -1
- novel_downloader/utils/crypto_utils/rc4.py +40 -50
- novel_downloader/utils/epub/__init__.py +2 -3
- novel_downloader/utils/epub/builder.py +6 -6
- novel_downloader/utils/epub/constants.py +1 -6
- novel_downloader/utils/epub/documents.py +7 -7
- novel_downloader/utils/epub/models.py +8 -8
- novel_downloader/utils/epub/utils.py +10 -10
- novel_downloader/utils/file_utils/io.py +48 -73
- novel_downloader/utils/file_utils/normalize.py +1 -7
- novel_downloader/utils/file_utils/sanitize.py +4 -11
- novel_downloader/utils/fontocr/__init__.py +13 -0
- novel_downloader/utils/{fontocr.py → fontocr/core.py} +72 -61
- novel_downloader/utils/fontocr/loader.py +52 -0
- novel_downloader/utils/logger.py +80 -56
- novel_downloader/utils/network.py +16 -40
- novel_downloader/utils/node_decryptor/__init__.py +13 -0
- novel_downloader/utils/node_decryptor/decryptor.py +342 -0
- novel_downloader/{core/parsers/qidian/utils → utils/node_decryptor}/decryptor_fetcher.py +5 -6
- novel_downloader/utils/text_utils/text_cleaner.py +39 -30
- novel_downloader/utils/text_utils/truncate_utils.py +3 -14
- novel_downloader/utils/time_utils/sleep_utils.py +53 -43
- novel_downloader/web/main.py +1 -1
- novel_downloader/web/pages/download.py +1 -1
- novel_downloader/web/pages/search.py +4 -4
- novel_downloader/web/services/task_manager.py +2 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.2.dist-info}/METADATA +5 -1
- novel_downloader-2.0.2.dist-info/RECORD +203 -0
- novel_downloader/core/exporters/common/__init__.py +0 -11
- novel_downloader/core/exporters/common/epub.py +0 -198
- novel_downloader/core/exporters/common/main_exporter.py +0 -64
- novel_downloader/core/exporters/common/txt.py +0 -146
- novel_downloader/core/exporters/epub_util.py +0 -215
- novel_downloader/core/exporters/linovelib/__init__.py +0 -11
- novel_downloader/core/exporters/linovelib/epub.py +0 -349
- novel_downloader/core/exporters/linovelib/main_exporter.py +0 -66
- novel_downloader/core/exporters/linovelib/txt.py +0 -139
- novel_downloader/core/exporters/txt_util.py +0 -67
- novel_downloader/core/parsers/qidian/__init__.py +0 -10
- novel_downloader/core/parsers/qidian/book_info_parser.py +0 -89
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -470
- novel_downloader/core/parsers/qidian/chapter_normal.py +0 -126
- novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader/core/parsers/qidian/main_parser.py +0 -101
- novel_downloader/core/parsers/qidian/utils/__init__.py +0 -30
- novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +0 -143
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -110
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +0 -175
- novel_downloader-2.0.0.dist-info/RECORD +0 -210
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.2.dist-info}/WHEEL +0 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.2.dist-info}/entry_points.txt +0 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.2.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.2.dist-info}/top_level.txt +0 -0
@@ -3,62 +3,52 @@
|
|
3
3
|
novel_downloader.utils.crypto_utils.rc4
|
4
4
|
---------------------------------------
|
5
5
|
|
6
|
-
RC4 stream cipher
|
6
|
+
Minimal RC4 stream cipher implementation.
|
7
7
|
"""
|
8
8
|
|
9
|
-
import base64
|
10
9
|
|
11
|
-
|
12
|
-
def rc4_crypt(
|
13
|
-
key: str,
|
14
|
-
data: str,
|
15
|
-
*,
|
16
|
-
mode: str = "encrypt",
|
17
|
-
encoding: str = "utf-8",
|
18
|
-
) -> str:
|
10
|
+
def rc4_init(key: bytes) -> list[int]:
|
19
11
|
"""
|
20
|
-
|
21
|
-
|
22
|
-
:param key: RC4 key (will be encoded using the specified encoding).
|
23
|
-
:param data: Plain-text (for 'encrypt') or Base64 cipher-text (for 'decrypt').
|
24
|
-
:param mode: Operation mode, either 'encrypt' or 'decrypt'. Defaults to 'encrypt'.
|
25
|
-
:param encoding: Character encoding for key and returned string. Defaults 'utf-8'.
|
26
|
-
|
27
|
-
:return: Base64 cipher-text (for encryption) or decoded plain-text (for decryption).
|
28
|
-
|
29
|
-
:raises ValueError: If mode is not 'encrypt' or 'decrypt'.
|
12
|
+
Key-Scheduling Algorithm (KSA)
|
30
13
|
"""
|
14
|
+
S = list(range(256))
|
15
|
+
j = 0
|
16
|
+
klen = len(key)
|
17
|
+
for i in range(256):
|
18
|
+
j = (j + S[i] + key[i % klen]) & 0xFF
|
19
|
+
S[i], S[j] = S[j], S[i]
|
20
|
+
return S
|
31
21
|
|
32
|
-
def _rc4(key_bytes: bytes, data_bytes: bytes) -> bytes:
|
33
|
-
# Key-Scheduling Algorithm (KSA)
|
34
|
-
S = list(range(256))
|
35
|
-
j = 0
|
36
|
-
for i in range(256):
|
37
|
-
j = (j + S[i] + key_bytes[i % len(key_bytes)]) % 256
|
38
|
-
S[i], S[j] = S[j], S[i]
|
39
|
-
|
40
|
-
# Pseudo-Random Generation Algorithm (PRGA)
|
41
|
-
i = j = 0
|
42
|
-
out: list[int] = []
|
43
|
-
for char in data_bytes:
|
44
|
-
i = (i + 1) % 256
|
45
|
-
j = (j + S[i]) % 256
|
46
|
-
S[i], S[j] = S[j], S[i]
|
47
|
-
K = S[(S[i] + S[j]) % 256]
|
48
|
-
out.append(char ^ K)
|
49
|
-
|
50
|
-
return bytes(out)
|
51
22
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
23
|
+
def rc4_stream(S_init: list[int], data: bytes) -> bytes:
|
24
|
+
"""
|
25
|
+
Pseudo-Random Generation Algorithm (PRGA)
|
26
|
+
"""
|
27
|
+
# make a copy of S since it mutates during PRGA
|
28
|
+
S = S_init.copy()
|
29
|
+
i = 0
|
30
|
+
j = 0
|
31
|
+
out = bytearray(len(data))
|
32
|
+
for idx, ch in enumerate(data):
|
33
|
+
i = (i + 1) & 0xFF
|
34
|
+
j = (j + S[i]) & 0xFF
|
35
|
+
S[i], S[j] = S[j], S[i]
|
36
|
+
K = S[(S[i] + S[j]) & 0xFF]
|
37
|
+
out[idx] = ch ^ K
|
38
|
+
|
39
|
+
return bytes(out)
|
40
|
+
|
41
|
+
|
42
|
+
def rc4_cipher(key: bytes, data: bytes) -> bytes:
|
43
|
+
"""
|
44
|
+
RC4 stream cipher.
|
58
45
|
|
59
|
-
|
60
|
-
|
61
|
-
plain_bytes = _rc4(key_bytes, cipher_bytes)
|
62
|
-
return plain_bytes.decode(encoding, errors="replace")
|
46
|
+
It performs the standard Key-Scheduling Algorithm (KSA) and
|
47
|
+
Pseudo-Random Generation Algorithm (PRGA) to produce the RC4 keystream.
|
63
48
|
|
64
|
-
|
49
|
+
:param key: RC4 key as bytes (must not be empty)
|
50
|
+
:param data: plaintext or ciphertext as bytes
|
51
|
+
:return: XORed bytes (encrypt/decrypt are identical)
|
52
|
+
"""
|
53
|
+
S = rc4_init(key)
|
54
|
+
return rc4_stream(S, data)
|
@@ -6,9 +6,8 @@ novel_downloader.utils.epub
|
|
6
6
|
Top-level package for EPUB export utilities.
|
7
7
|
|
8
8
|
Key components:
|
9
|
-
|
10
|
-
|
11
|
-
- Chapter, Volume : represent and render content sections and volume intros
|
9
|
+
* EpubBuilder : orchestrates metadata, manifest, spine, navigation, and resources
|
10
|
+
* Chapter, Volume : represent and render content sections and volume intros
|
12
11
|
|
13
12
|
Usage example:
|
14
13
|
|
@@ -4,14 +4,14 @@ novel_downloader.utils.epub.builder
|
|
4
4
|
-----------------------------------
|
5
5
|
|
6
6
|
Orchestrates the end-to-end EPUB build process by:
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
* Managing metadata (title, author, description, language, etc.)
|
8
|
+
* Collecting and deduplicating resources (chapters, images, stylesheets)
|
9
|
+
* Registering everything in the OPF manifest and spine
|
10
|
+
* Generating nav.xhtml, toc.ncx, content.opf, and the zipped .epub file
|
11
11
|
|
12
12
|
Provides:
|
13
|
-
|
14
|
-
|
13
|
+
* methods to add chapters, volumes, images, and styles
|
14
|
+
* a clean `export()` entry point that writes the final EPUB archive
|
15
15
|
"""
|
16
16
|
|
17
17
|
import zipfile
|
@@ -3,12 +3,7 @@
|
|
3
3
|
novel_downloader.utils.epub.constants
|
4
4
|
-------------------------------------
|
5
5
|
|
6
|
-
EPUB-specific constants used by the builder
|
7
|
-
- Directory names for OEBPS structure
|
8
|
-
- XML namespace URIs
|
9
|
-
- Package attributes and document-type declarations
|
10
|
-
- Media type mappings for images
|
11
|
-
- Template strings for container.xml and cover image HTML
|
6
|
+
EPUB-specific constants used by the builder.
|
12
7
|
"""
|
13
8
|
|
14
9
|
ROOT_PATH = "OEBPS"
|
@@ -4,9 +4,9 @@ novel_downloader.utils.epub.documents
|
|
4
4
|
-------------------------------------
|
5
5
|
|
6
6
|
Defines the classes that render EPUB navigation and packaging documents:
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
* NavDocument: builds the XHTML nav.xhtml (EPUB 3)
|
8
|
+
* NCXDocument: builds the NCX XML navigation map (EPUB 2)
|
9
|
+
* OpfDocument: builds the content.opf package document
|
10
10
|
"""
|
11
11
|
|
12
12
|
from collections.abc import Sequence
|
@@ -234,10 +234,10 @@ class OpfDocument(EpubResource):
|
|
234
234
|
Generate the content.opf XML, which defines metadata, manifest, and spine.
|
235
235
|
|
236
236
|
This function outputs a complete OPF package document that includes:
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
237
|
+
* <metadata>: title, author, language, identifiers, etc.
|
238
|
+
* <manifest>: all resource entries
|
239
|
+
* <spine>: the reading order of the content
|
240
|
+
* <guide>: optional references like cover page
|
241
241
|
|
242
242
|
:return: A string containing the full OPF XML content.
|
243
243
|
"""
|
@@ -4,14 +4,14 @@ novel_downloader.utils.epub.models
|
|
4
4
|
----------------------------------
|
5
5
|
|
6
6
|
Defines the core EPUB data models and resource classes used by the builder:
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
* Typed entries for table of contents (ChapterEntry, VolumeEntry)
|
8
|
+
* Manifest and spine record types (ManifestEntry, SpineEntry)
|
9
|
+
* Hierarchical NavPoint for NCX navigation
|
10
|
+
* Base resource class (EpubResource) and specializations:
|
11
|
+
* StyleSheet
|
12
|
+
* ImageResource
|
13
|
+
* Chapter (with XHTML serialization)
|
14
|
+
* Volume container for grouping chapters with optional intro and cover
|
15
15
|
"""
|
16
16
|
|
17
17
|
from __future__ import annotations
|
@@ -4,9 +4,9 @@ novel_downloader.utils.epub.utils
|
|
4
4
|
---------------------------------
|
5
5
|
|
6
6
|
Pure utility functions for EPUB assembly, including:
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
* Computing file hashes
|
8
|
+
* Generating META-INF/container.xml
|
9
|
+
* Constructing HTML snippets for the book intro and volume intro
|
10
10
|
"""
|
11
11
|
|
12
12
|
import hashlib
|
@@ -59,9 +59,9 @@ def build_book_intro(
|
|
59
59
|
Build the HTML snippet for the overall book introduction.
|
60
60
|
|
61
61
|
This includes:
|
62
|
-
|
63
|
-
|
64
|
-
|
62
|
+
* A main heading ("Book Introduction")
|
63
|
+
* A list of metadata items (title, author, categories, word count, status)
|
64
|
+
* A "Summary" subheading and one or more paragraphs of summary text
|
65
65
|
|
66
66
|
:return: A HTML string for inclusion in `intro.xhtml`
|
67
67
|
"""
|
@@ -112,10 +112,10 @@ def build_volume_intro(
|
|
112
112
|
Build the HTML snippet for a single-volume introduction.
|
113
113
|
|
114
114
|
This includes:
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
115
|
+
* A decorative border image (top and bottom)
|
116
|
+
* A primary heading (volume main title)
|
117
|
+
* An optional secondary line (subtitle)
|
118
|
+
* One or more paragraphs of intro text
|
119
119
|
|
120
120
|
:param volume_title: e.g. "Volume 1 - The Beginning"
|
121
121
|
:param volume_intro_text: multiline intro text for this volume
|
@@ -8,99 +8,74 @@ File I/O utilities for reading and writing data.
|
|
8
8
|
|
9
9
|
__all__ = ["write_file"]
|
10
10
|
|
11
|
-
import json
|
12
|
-
import logging
|
13
11
|
import tempfile
|
14
12
|
from pathlib import Path
|
15
|
-
from typing import
|
13
|
+
from typing import Literal
|
16
14
|
|
17
15
|
from .sanitize import sanitize_filename
|
18
16
|
|
19
|
-
logger = logging.getLogger(__name__)
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
def _get_non_conflicting_path(path: Path) -> Path:
|
18
|
+
def _unique_path(path: Path, max_tries: int = 100) -> Path:
|
25
19
|
"""
|
26
|
-
|
20
|
+
Return a unique file path by appending _1, _2, ... if needed.
|
21
|
+
|
22
|
+
Falls back to a UUID suffix if all attempts fail.
|
27
23
|
"""
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
24
|
+
if not path.exists():
|
25
|
+
return path
|
26
|
+
|
27
|
+
stem = path.stem
|
28
|
+
suffix = path.suffix
|
29
|
+
|
30
|
+
for counter in range(1, max_tries + 1):
|
31
|
+
candidate = path.with_name(f"{stem}_{counter}{suffix}")
|
32
|
+
if not candidate.exists():
|
33
|
+
return candidate
|
34
|
+
|
35
|
+
# fallback: append a random/unique suffix
|
36
|
+
import uuid
|
37
|
+
|
38
|
+
return path.with_name(f"{stem}_{uuid.uuid4().hex}{suffix}")
|
36
39
|
|
37
40
|
|
38
41
|
def write_file(
|
39
|
-
content: str | bytes
|
42
|
+
content: str | bytes,
|
40
43
|
filepath: str | Path,
|
41
|
-
write_mode: str = "w",
|
42
44
|
*,
|
43
45
|
on_exist: Literal["overwrite", "skip", "rename"] = "overwrite",
|
44
|
-
dump_json: bool = False,
|
45
46
|
encoding: str = "utf-8",
|
46
|
-
) -> Path
|
47
|
+
) -> Path:
|
47
48
|
"""
|
48
|
-
Write content to a file safely with
|
49
|
-
|
50
|
-
|
51
|
-
:param
|
52
|
-
|
53
|
-
:param filepath: Destination path (str or Path).
|
54
|
-
:param mode: File mode ('w', 'wb'). Auto-determined if None.
|
55
|
-
:param on_exist: Behavior if file exists: 'overwrite', 'skip',
|
56
|
-
or 'rename'.
|
57
|
-
:param dump_json: If True, serialize content as JSON.
|
49
|
+
Write content to a file safely with atomic replacement.
|
50
|
+
|
51
|
+
:param content: The content to write; can be text or bytes.
|
52
|
+
:param filepath: Destination path.
|
53
|
+
:param on_exist: Behavior if file exists.
|
58
54
|
:param encoding: Text encoding for writing.
|
59
|
-
:return:
|
55
|
+
:return: The final path where the content was written.
|
56
|
+
:raise: Any I/O error such as PermissionError or OSError
|
60
57
|
"""
|
61
58
|
path = Path(filepath)
|
62
59
|
path = path.with_name(sanitize_filename(path.name))
|
63
60
|
path.parent.mkdir(parents=True, exist_ok=True)
|
64
61
|
|
65
62
|
if path.exists():
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
if isinstance(content, (str | bytes)):
|
86
|
-
content_to_write = content
|
87
|
-
else:
|
88
|
-
raise TypeError("Non-JSON content must be str or bytes.")
|
89
|
-
write_mode = "wb" if isinstance(content, bytes) else "w"
|
90
|
-
|
91
|
-
try:
|
92
|
-
with tempfile.NamedTemporaryFile(
|
93
|
-
mode=write_mode,
|
94
|
-
encoding=None if "b" in write_mode else encoding,
|
95
|
-
newline=None if "b" in write_mode else "\n",
|
96
|
-
delete=False,
|
97
|
-
dir=path.parent,
|
98
|
-
) as tmp:
|
99
|
-
tmp.write(content_to_write)
|
100
|
-
tmp_path = Path(tmp.name)
|
101
|
-
tmp_path.replace(path)
|
102
|
-
logger.debug("[file] '%s' written successfully", path)
|
103
|
-
return path
|
104
|
-
except Exception as exc:
|
105
|
-
logger.warning("[file] Error writing %r: %s", path, exc)
|
106
|
-
return None
|
63
|
+
match on_exist:
|
64
|
+
case "skip":
|
65
|
+
return path
|
66
|
+
case "rename":
|
67
|
+
path = _unique_path(path)
|
68
|
+
|
69
|
+
write_mode = "wb" if isinstance(content, bytes) else "w"
|
70
|
+
|
71
|
+
with tempfile.NamedTemporaryFile(
|
72
|
+
mode=write_mode,
|
73
|
+
encoding=None if "b" in write_mode else encoding,
|
74
|
+
newline=None if "b" in write_mode else "\n",
|
75
|
+
delete=False,
|
76
|
+
dir=path.parent,
|
77
|
+
) as tmp:
|
78
|
+
tmp.write(content)
|
79
|
+
tmp_path = Path(tmp.name)
|
80
|
+
tmp_path.replace(path)
|
81
|
+
return path
|
@@ -14,8 +14,6 @@ __all__ = ["normalize_txt_line_endings"]
|
|
14
14
|
import logging
|
15
15
|
from pathlib import Path
|
16
16
|
|
17
|
-
logger = logging.getLogger(__name__)
|
18
|
-
|
19
17
|
|
20
18
|
def normalize_txt_line_endings(folder_path: str | Path) -> None:
|
21
19
|
"""
|
@@ -28,7 +26,6 @@ def normalize_txt_line_endings(folder_path: str | Path) -> None:
|
|
28
26
|
"""
|
29
27
|
path = Path(folder_path).resolve()
|
30
28
|
if not path.exists() or not path.is_dir():
|
31
|
-
logger.warning("[file] Invalid folder: %s", path)
|
32
29
|
return
|
33
30
|
|
34
31
|
count_success, count_fail = 0, 0
|
@@ -38,13 +35,10 @@ def normalize_txt_line_endings(folder_path: str | Path) -> None:
|
|
38
35
|
content = txt_file.read_text(encoding="utf-8")
|
39
36
|
normalized = content.replace("\r\n", "\n").replace("\r", "\n")
|
40
37
|
txt_file.write_text(normalized, encoding="utf-8", newline="\n")
|
41
|
-
logger.debug("[file] Normalized: %s", txt_file)
|
42
38
|
count_success += 1
|
43
|
-
except (OSError, UnicodeDecodeError)
|
44
|
-
logger.warning("[file] Failed: %s | %s", txt_file, e)
|
39
|
+
except (OSError, UnicodeDecodeError):
|
45
40
|
count_fail += 1
|
46
41
|
|
47
|
-
logger.info("[file] Completed. Success: %s, Failed: %s", count_success, count_fail)
|
48
42
|
return
|
49
43
|
|
50
44
|
|
@@ -9,13 +9,9 @@ on different operating systems.
|
|
9
9
|
|
10
10
|
__all__ = ["sanitize_filename"]
|
11
11
|
|
12
|
-
import logging
|
13
12
|
import os
|
14
13
|
import re
|
15
14
|
|
16
|
-
logger = logging.getLogger(__name__)
|
17
|
-
|
18
|
-
# Windows 保留名称列表 (忽略大小写)
|
19
15
|
_WIN_RESERVED_NAMES = {
|
20
16
|
"CON",
|
21
17
|
"PRN",
|
@@ -36,8 +32,8 @@ def sanitize_filename(filename: str, max_length: int | None = 255) -> str:
|
|
36
32
|
|
37
33
|
This function checks the operating system environment and applies the appropriate
|
38
34
|
filtering rules:
|
39
|
-
|
40
|
-
|
35
|
+
* On Windows, it replaces characters: <>:"/\\|?*
|
36
|
+
* On POSIX systems, it replaces the forward slash '/'
|
41
37
|
|
42
38
|
:param filename: The input filename to sanitize.
|
43
39
|
:param max_length: Optional maximum length of the output filename. Defaults to 255.
|
@@ -47,7 +43,7 @@ def sanitize_filename(filename: str, max_length: int | None = 255) -> str:
|
|
47
43
|
|
48
44
|
name = pattern.sub("_", filename).strip(" .")
|
49
45
|
|
50
|
-
stem, dot, ext = name.
|
46
|
+
stem, dot, ext = name.rpartition(".")
|
51
47
|
if os.name == "nt" and stem.upper() in _WIN_RESERVED_NAMES:
|
52
48
|
stem = f"_{stem}"
|
53
49
|
cleaned = f"{stem}{dot}{ext}" if ext else stem
|
@@ -59,7 +55,4 @@ def sanitize_filename(filename: str, max_length: int | None = 255) -> str:
|
|
59
55
|
else:
|
60
56
|
cleaned = cleaned[:max_length]
|
61
57
|
|
62
|
-
|
63
|
-
cleaned = "_untitled"
|
64
|
-
logger.debug("[file] Sanitized filename: %r -> %r", filename, cleaned)
|
65
|
-
return cleaned
|
58
|
+
return cleaned or "_untitled"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.fontocr
|
4
|
+
------------------------------
|
5
|
+
|
6
|
+
Lazy-loading interface for FontOCR. Provides a safe entry point
|
7
|
+
to obtain an OCR utility instance if optional dependencies are available.
|
8
|
+
"""
|
9
|
+
|
10
|
+
__all__ = ["get_font_ocr"]
|
11
|
+
__version__ = "4.0"
|
12
|
+
|
13
|
+
from .loader import get_font_ocr
|