ranobelib-python-sdk 0.2.0__tar.gz → 0.4.0__tar.gz
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.
- ranobelib_python_sdk-0.4.0/PKG-INFO +113 -0
- ranobelib_python_sdk-0.4.0/README.md +84 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/pyproject.toml +3 -1
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/pyproject.toml.orig +3 -1
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/__init__.py +3 -0
- ranobelib_python_sdk-0.4.0/src/ranobelib/console.py +72 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/exporters/__init__.py +13 -1
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/exporters/epub.py +13 -1
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/exporters/fb2.py +14 -3
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/exporters/pdf.py +18 -4
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/exporters/txt.py +12 -1
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/sdk.py +111 -5
- ranobelib_python_sdk-0.4.0/src/ranobelib/sizing.py +62 -0
- ranobelib_python_sdk-0.2.0/PKG-INFO +0 -478
- ranobelib_python_sdk-0.2.0/README.md +0 -451
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/cache.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/client.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/exceptions.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/exporters/_illustrations.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/exporters/_shared.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/models.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/numbering.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/py.typed +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ranobelib-python-sdk
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Async Python SDK for ranobelib.me: title metadata, chapter/volume downloads, and export to epub/fb2/txt/pdf
|
|
5
|
+
Keywords: ranobelib,ranobe,novel,webnovel,epub,fb2,sdk
|
|
6
|
+
Author: LLIJIYAPNIK
|
|
7
|
+
Author-email: LLIJIYAPNIK <kas4wot@yandex.ru>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: ebooklib>=0.20
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: pydantic>=2.7
|
|
21
|
+
Requires-Dist: rich>=15.0.0
|
|
22
|
+
Requires-Dist: weasyprint>=69.0
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Project-URL: Homepage, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk
|
|
25
|
+
Project-URL: Repository, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk
|
|
26
|
+
Project-URL: Issues, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/issues
|
|
27
|
+
Project-URL: Changelog, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/blob/main/CHANGELOG.md
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# ranobelib-python-sdk
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/ranobelib-python-sdk/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
[](pyproject.toml)
|
|
35
|
+
[](https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/actions/workflows/ci.yml)
|
|
36
|
+
[](https://coveralls.io/github/LLIJIYAPNIK/ranobelib-python-sdk?branch=main)
|
|
37
|
+
[](https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/actions/workflows/docs.yml)
|
|
38
|
+
|
|
39
|
+
Async Python SDK for [ranobelib.me](https://ranobelib.me): fetch title metadata, download
|
|
40
|
+
chapters and volumes, and export to epub, fb2, txt, or pdf.
|
|
41
|
+
|
|
42
|
+
The SDK talks to the undocumented but open JSON API behind the site (`api.cdnlibs.org`,
|
|
43
|
+
part of the lib.social network) instead of scraping HTML.
|
|
44
|
+
|
|
45
|
+
> **Status:** all the calls in the quickstart below are implemented — metadata, table of
|
|
46
|
+
> contents, single/bulk chapter and volume downloads, whole-title bulk download, translation
|
|
47
|
+
> selection, and export to
|
|
48
|
+
> `"txt"`, `"fb2"`, `"epub"`, or `"pdf"` (cover + in-chapter illustrations embedded in
|
|
49
|
+
> epub/pdf). Raw API responses are cached to disk, and the underlying client bounds
|
|
50
|
+
> concurrency, paces requests, and retries 429/5xx with backoff. `fmt="pdf"` needs
|
|
51
|
+
> WeasyPrint's native dependencies (Pango/cairo/GTK3) installed on the system — `pip
|
|
52
|
+
> install`/`uv add` alone doesn't guarantee that on every platform; without them,
|
|
53
|
+
> `export(fmt="pdf")` raises the same "unknown format" error the other three formats don't.
|
|
54
|
+
> Authorization (private/18+/paid content) is out of scope — see `CLAUDE.md` for the full
|
|
55
|
+
> architecture and design decisions behind all of this.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv add ranobelib-python-sdk
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
or
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install ranobelib-python-sdk
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Requires Python 3.11+.
|
|
70
|
+
|
|
71
|
+
## Quickstart
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from ranobelib import RanobeLib
|
|
75
|
+
|
|
76
|
+
async with RanobeLib("https://ranobelib.me/ru/book/6712--high-school-dxd-novel") as lib:
|
|
77
|
+
info = await lib.get_info()
|
|
78
|
+
toc = await lib.get_table_of_contents()
|
|
79
|
+
|
|
80
|
+
chapter = await lib.get_chapter(volume=6, number="51.6")
|
|
81
|
+
volume = await lib.get_volume(volume=6)
|
|
82
|
+
|
|
83
|
+
chapters = await lib.get_chapters([(6, "51.6"), (6, "52")])
|
|
84
|
+
volumes = await lib.get_volumes([1, 2, 3])
|
|
85
|
+
|
|
86
|
+
translations = await lib.get_translations(volume=6, number="51.6")
|
|
87
|
+
chapter = await lib.get_chapter(volume=6, number="51.6", branch_id=...)
|
|
88
|
+
|
|
89
|
+
all_volumes = await lib.download_title(translation_index=0, chapter_delay=0.5)
|
|
90
|
+
|
|
91
|
+
await lib.export(chapters, fmt="epub", path="output.epub")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This is the full current public API — everything above is implemented and works today.
|
|
95
|
+
|
|
96
|
+
## Examples
|
|
97
|
+
|
|
98
|
+
Complete, runnable scripts for every feature above — title metadata, table of contents,
|
|
99
|
+
single/bulk chapter and volume downloads, translation selection, whole-title bulk download,
|
|
100
|
+
disk caching, export to all four formats, and error handling — live in
|
|
101
|
+
**[`examples/`](examples/)**, one heavily commented file per feature. Start there for
|
|
102
|
+
copy-pasteable code.
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
|
|
106
|
+
Full API reference and guides: **<https://LLIJIYAPNIK.github.io/ranobelib-python-sdk/>**
|
|
107
|
+
(built with `mkdocs-material` + `mkdocstrings`, deployed on push to `main`). `CLAUDE.md`
|
|
108
|
+
documents the full architecture, public API contract, and the reasoning behind the SDK's
|
|
109
|
+
design decisions. [`CHANGELOG.md`](CHANGELOG.md) lists what changed in each release.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ranobelib-python-sdk
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/ranobelib-python-sdk/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](pyproject.toml)
|
|
6
|
+
[](https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/actions/workflows/ci.yml)
|
|
7
|
+
[](https://coveralls.io/github/LLIJIYAPNIK/ranobelib-python-sdk?branch=main)
|
|
8
|
+
[](https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/actions/workflows/docs.yml)
|
|
9
|
+
|
|
10
|
+
Async Python SDK for [ranobelib.me](https://ranobelib.me): fetch title metadata, download
|
|
11
|
+
chapters and volumes, and export to epub, fb2, txt, or pdf.
|
|
12
|
+
|
|
13
|
+
The SDK talks to the undocumented but open JSON API behind the site (`api.cdnlibs.org`,
|
|
14
|
+
part of the lib.social network) instead of scraping HTML.
|
|
15
|
+
|
|
16
|
+
> **Status:** all the calls in the quickstart below are implemented — metadata, table of
|
|
17
|
+
> contents, single/bulk chapter and volume downloads, whole-title bulk download, translation
|
|
18
|
+
> selection, and export to
|
|
19
|
+
> `"txt"`, `"fb2"`, `"epub"`, or `"pdf"` (cover + in-chapter illustrations embedded in
|
|
20
|
+
> epub/pdf). Raw API responses are cached to disk, and the underlying client bounds
|
|
21
|
+
> concurrency, paces requests, and retries 429/5xx with backoff. `fmt="pdf"` needs
|
|
22
|
+
> WeasyPrint's native dependencies (Pango/cairo/GTK3) installed on the system — `pip
|
|
23
|
+
> install`/`uv add` alone doesn't guarantee that on every platform; without them,
|
|
24
|
+
> `export(fmt="pdf")` raises the same "unknown format" error the other three formats don't.
|
|
25
|
+
> Authorization (private/18+/paid content) is out of scope — see `CLAUDE.md` for the full
|
|
26
|
+
> architecture and design decisions behind all of this.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv add ranobelib-python-sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
or
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ranobelib-python-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Python 3.11+.
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from ranobelib import RanobeLib
|
|
46
|
+
|
|
47
|
+
async with RanobeLib("https://ranobelib.me/ru/book/6712--high-school-dxd-novel") as lib:
|
|
48
|
+
info = await lib.get_info()
|
|
49
|
+
toc = await lib.get_table_of_contents()
|
|
50
|
+
|
|
51
|
+
chapter = await lib.get_chapter(volume=6, number="51.6")
|
|
52
|
+
volume = await lib.get_volume(volume=6)
|
|
53
|
+
|
|
54
|
+
chapters = await lib.get_chapters([(6, "51.6"), (6, "52")])
|
|
55
|
+
volumes = await lib.get_volumes([1, 2, 3])
|
|
56
|
+
|
|
57
|
+
translations = await lib.get_translations(volume=6, number="51.6")
|
|
58
|
+
chapter = await lib.get_chapter(volume=6, number="51.6", branch_id=...)
|
|
59
|
+
|
|
60
|
+
all_volumes = await lib.download_title(translation_index=0, chapter_delay=0.5)
|
|
61
|
+
|
|
62
|
+
await lib.export(chapters, fmt="epub", path="output.epub")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This is the full current public API — everything above is implemented and works today.
|
|
66
|
+
|
|
67
|
+
## Examples
|
|
68
|
+
|
|
69
|
+
Complete, runnable scripts for every feature above — title metadata, table of contents,
|
|
70
|
+
single/bulk chapter and volume downloads, translation selection, whole-title bulk download,
|
|
71
|
+
disk caching, export to all four formats, and error handling — live in
|
|
72
|
+
**[`examples/`](examples/)**, one heavily commented file per feature. Start there for
|
|
73
|
+
copy-pasteable code.
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
Full API reference and guides: **<https://LLIJIYAPNIK.github.io/ranobelib-python-sdk/>**
|
|
78
|
+
(built with `mkdocs-material` + `mkdocstrings`, deployed on push to `main`). `CLAUDE.md`
|
|
79
|
+
documents the full architecture, public API contract, and the reasoning behind the SDK's
|
|
80
|
+
design decisions. [`CHANGELOG.md`](CHANGELOG.md) lists what changed in each release.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ranobelib-python-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Async Python SDK for ranobelib.me: title metadata, chapter/volume downloads, and export to epub/fb2/txt/pdf"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
"ebooklib>=0.20",
|
|
30
30
|
"httpx>=0.27",
|
|
31
31
|
"pydantic>=2.7",
|
|
32
|
+
"rich>=15.0.0",
|
|
32
33
|
"weasyprint>=69.0",
|
|
33
34
|
]
|
|
34
35
|
|
|
@@ -40,6 +41,7 @@ email = "kas4wot@yandex.ru"
|
|
|
40
41
|
Homepage = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk"
|
|
41
42
|
Repository = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk"
|
|
42
43
|
Issues = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/issues"
|
|
44
|
+
Changelog = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/blob/main/CHANGELOG.md"
|
|
43
45
|
|
|
44
46
|
[build-system]
|
|
45
47
|
requires = ["uv_build>=0.11.26,<0.12.0"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ranobelib-python-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Async Python SDK for ranobelib.me: title metadata, chapter/volume downloads, and export to epub/fb2/txt/pdf"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -24,6 +24,7 @@ dependencies = [
|
|
|
24
24
|
"ebooklib>=0.20",
|
|
25
25
|
"httpx>=0.27",
|
|
26
26
|
"pydantic>=2.7",
|
|
27
|
+
"rich>=15.0.0",
|
|
27
28
|
"weasyprint>=69.0",
|
|
28
29
|
]
|
|
29
30
|
|
|
@@ -31,6 +32,7 @@ dependencies = [
|
|
|
31
32
|
Homepage = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk"
|
|
32
33
|
Repository = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk"
|
|
33
34
|
Issues = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/issues"
|
|
35
|
+
Changelog = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/blob/main/CHANGELOG.md"
|
|
34
36
|
|
|
35
37
|
[build-system]
|
|
36
38
|
requires = ["uv_build>=0.11.26,<0.12.0"]
|
|
@@ -13,6 +13,7 @@ from ranobelib.exceptions import (
|
|
|
13
13
|
)
|
|
14
14
|
from ranobelib.models import Chapter, ChapterBranch, Title, Volume
|
|
15
15
|
from ranobelib.sdk import RanobeLib
|
|
16
|
+
from ranobelib.sizing import chapter_size, volume_size
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"AmbiguousChapter",
|
|
@@ -29,4 +30,6 @@ __all__ = [
|
|
|
29
30
|
"TitleNotFoundError",
|
|
30
31
|
"Volume",
|
|
31
32
|
"VolumeNotFoundError",
|
|
33
|
+
"chapter_size",
|
|
34
|
+
"volume_size",
|
|
32
35
|
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Console output for RanobeLib: progress bars and full-mode operation logging.
|
|
2
|
+
|
|
3
|
+
Behind ``RanobeLib``'s ``verbosity`` parameter (``False`` by default, ``"progress_only"``,
|
|
4
|
+
or ``"full"`` — see CLAUDE.md's roadmap entry for why these three, and why this prints
|
|
5
|
+
directly via ``rich`` rather than going through ``logging``). ``Reporter`` is the single
|
|
6
|
+
object that knows which of the three modes is active; every call site (``sdk.py``, the
|
|
7
|
+
exporters) just calls ``log()``/``progress()`` unconditionally and lets ``Reporter`` decide
|
|
8
|
+
whether that produces any output.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable, Iterator
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn, TimeElapsedColumn
|
|
19
|
+
|
|
20
|
+
Verbosity = Literal["full", "progress_only"] | Literal[False]
|
|
21
|
+
"""``False``: silent. ``"progress_only"``: progress bars only. ``"full"``: progress bars
|
|
22
|
+
plus a line logged for every title/chapters-list/chapter-content fetch (cache hit or API)."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Reporter:
|
|
26
|
+
"""Console output gated by a ``Verbosity`` level, backed by a single ``rich.Console``."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, verbosity: Verbosity, *, console: Console | None = None) -> None:
|
|
29
|
+
"""Initialize the reporter.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
verbosity: The active output level.
|
|
33
|
+
console: Custom ``rich.console.Console`` to print through, e.g. one writing to
|
|
34
|
+
an in-memory stream for tests. Defaults to a new ``Console()`` (stdout) —
|
|
35
|
+
only constructed at all when ``verbosity`` isn't ``False``, so a silent
|
|
36
|
+
``RanobeLib`` never touches the terminal.
|
|
37
|
+
"""
|
|
38
|
+
self._verbosity = verbosity
|
|
39
|
+
if not verbosity:
|
|
40
|
+
self._console = None
|
|
41
|
+
else:
|
|
42
|
+
self._console = console if console is not None else Console()
|
|
43
|
+
|
|
44
|
+
def log(self, message: str) -> None:
|
|
45
|
+
"""Print ``message``, only when verbosity is ``"full"``.
|
|
46
|
+
|
|
47
|
+
``markup=False``: messages embed literal ``[cache]``/``[api]`` prefixes (see
|
|
48
|
+
``sdk.py``'s choke points), which rich's default markup parsing would otherwise try
|
|
49
|
+
to interpret as style tags.
|
|
50
|
+
"""
|
|
51
|
+
if self._verbosity == "full" and self._console is not None:
|
|
52
|
+
self._console.log(message, markup=False)
|
|
53
|
+
|
|
54
|
+
@contextmanager
|
|
55
|
+
def progress(self, description: str, total: int) -> Iterator[Callable[[], None]]:
|
|
56
|
+
"""A progress bar shown for ``"progress_only"``/``"full"``, a no-op otherwise.
|
|
57
|
+
|
|
58
|
+
Yields a zero-argument callable: call it once per completed step.
|
|
59
|
+
"""
|
|
60
|
+
if not self._verbosity or total == 0:
|
|
61
|
+
yield lambda: None
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
with Progress(
|
|
65
|
+
TextColumn("[progress.description]{task.description}"),
|
|
66
|
+
BarColumn(),
|
|
67
|
+
MofNCompleteColumn(),
|
|
68
|
+
TimeElapsedColumn(),
|
|
69
|
+
console=self._console,
|
|
70
|
+
) as progress:
|
|
71
|
+
task_id = progress.add_task(description, total=total)
|
|
72
|
+
yield lambda: progress.advance(task_id)
|
{ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.4.0}/src/ranobelib/exporters/__init__.py
RENAMED
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import importlib
|
|
11
11
|
import pkgutil
|
|
12
|
+
from collections.abc import Callable
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import ClassVar, Protocol, TypeVar, runtime_checkable
|
|
14
15
|
|
|
@@ -22,7 +23,14 @@ class Exporter(Protocol):
|
|
|
22
23
|
format: ClassVar[str]
|
|
23
24
|
"""The registry key this exporter is selected by, e.g. ``"txt"``."""
|
|
24
25
|
|
|
25
|
-
async def export(
|
|
26
|
+
async def export(
|
|
27
|
+
self,
|
|
28
|
+
title: Title,
|
|
29
|
+
chapters: list[Chapter],
|
|
30
|
+
output_path: Path,
|
|
31
|
+
*,
|
|
32
|
+
on_chapter: Callable[[], None] | None = None,
|
|
33
|
+
) -> Path:
|
|
26
34
|
"""Write ``chapters`` (in the given order) to ``output_path``.
|
|
27
35
|
|
|
28
36
|
``async`` since embedding illustrations (epub, pdf) requires downloading them —
|
|
@@ -33,6 +41,10 @@ class Exporter(Protocol):
|
|
|
33
41
|
title: The chapters' parent title, for metadata (name, authors, ...).
|
|
34
42
|
chapters: The chapters to include, in the order they should appear.
|
|
35
43
|
output_path: Where to write the exported file.
|
|
44
|
+
on_chapter: Called once per chapter processed, if given — drives
|
|
45
|
+
``RanobeLib.export()``'s progress bar (see CLAUDE.md's roadmap step 23).
|
|
46
|
+
For epub/pdf this covers the per-chapter embedding step, not the earlier
|
|
47
|
+
illustration-download step, which isn't itself progress-reported.
|
|
36
48
|
|
|
37
49
|
Returns:
|
|
38
50
|
``output_path``, once the file has been written.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import html
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import ClassVar
|
|
8
9
|
|
|
@@ -39,7 +40,14 @@ class EpubExporter:
|
|
|
39
40
|
"""
|
|
40
41
|
self._transport = transport
|
|
41
42
|
|
|
42
|
-
async def export(
|
|
43
|
+
async def export(
|
|
44
|
+
self,
|
|
45
|
+
title: Title,
|
|
46
|
+
chapters: list[Chapter],
|
|
47
|
+
output_path: Path,
|
|
48
|
+
*,
|
|
49
|
+
on_chapter: Callable[[], None] | None = None,
|
|
50
|
+
) -> Path:
|
|
43
51
|
"""Write ``chapters`` to ``output_path`` as an EPUB file.
|
|
44
52
|
|
|
45
53
|
Downloads the title's cover and every image referenced in the chapters' content and
|
|
@@ -50,6 +58,8 @@ class EpubExporter:
|
|
|
50
58
|
title: The chapters' parent title; supplies metadata, cover, and authors.
|
|
51
59
|
chapters: The chapters to include, in the order they should appear.
|
|
52
60
|
output_path: Where to write the ``.epub`` file.
|
|
61
|
+
on_chapter: Called once per chapter embedded, if given — after illustrations
|
|
62
|
+
have already been downloaded, see the ``Exporter`` protocol's docstring.
|
|
53
63
|
|
|
54
64
|
Returns:
|
|
55
65
|
``output_path``.
|
|
@@ -107,6 +117,8 @@ class EpubExporter:
|
|
|
107
117
|
book.add_item(doc)
|
|
108
118
|
spine.append(doc)
|
|
109
119
|
toc.append(doc)
|
|
120
|
+
if on_chapter is not None:
|
|
121
|
+
on_chapter()
|
|
110
122
|
|
|
111
123
|
book.toc = toc
|
|
112
124
|
book.add_item(epub.EpubNcx())
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
6
|
import xml.etree.ElementTree as ET
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from html.parser import HTMLParser
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import ClassVar
|
|
@@ -126,7 +127,7 @@ def _build_description(title: Title) -> ET.Element:
|
|
|
126
127
|
return description
|
|
127
128
|
|
|
128
129
|
|
|
129
|
-
def _build_body(chapters: list[Chapter]) -> ET.Element:
|
|
130
|
+
def _build_body(chapters: list[Chapter], on_chapter: Callable[[], None] | None) -> ET.Element:
|
|
130
131
|
body = ET.Element(_tag("body"))
|
|
131
132
|
for chapter in chapters:
|
|
132
133
|
section = ET.SubElement(body, _tag("section"))
|
|
@@ -134,6 +135,8 @@ def _build_body(chapters: list[Chapter]) -> ET.Element:
|
|
|
134
135
|
ET.SubElement(section_title, _tag("p")).text = chapter_heading(chapter)
|
|
135
136
|
for paragraph in html_to_fb2_paragraphs(chapter.content or ""):
|
|
136
137
|
section.append(paragraph)
|
|
138
|
+
if on_chapter is not None:
|
|
139
|
+
on_chapter()
|
|
137
140
|
return body
|
|
138
141
|
|
|
139
142
|
|
|
@@ -143,20 +146,28 @@ class Fb2Exporter:
|
|
|
143
146
|
|
|
144
147
|
format: ClassVar[str] = "fb2"
|
|
145
148
|
|
|
146
|
-
async def export(
|
|
149
|
+
async def export(
|
|
150
|
+
self,
|
|
151
|
+
title: Title,
|
|
152
|
+
chapters: list[Chapter],
|
|
153
|
+
output_path: Path,
|
|
154
|
+
*,
|
|
155
|
+
on_chapter: Callable[[], None] | None = None,
|
|
156
|
+
) -> Path:
|
|
147
157
|
"""Write ``chapters`` to ``output_path`` as FB2 XML.
|
|
148
158
|
|
|
149
159
|
Args:
|
|
150
160
|
title: The chapters' parent title; supplies ``description`` metadata.
|
|
151
161
|
chapters: The chapters to include, in the order they should appear.
|
|
152
162
|
output_path: Where to write the ``.fb2`` file.
|
|
163
|
+
on_chapter: Called once per chapter written, if given.
|
|
153
164
|
|
|
154
165
|
Returns:
|
|
155
166
|
``output_path``.
|
|
156
167
|
"""
|
|
157
168
|
root = ET.Element(_tag("FictionBook"))
|
|
158
169
|
root.append(_build_description(title))
|
|
159
|
-
root.append(_build_body(chapters))
|
|
170
|
+
root.append(_build_body(chapters, on_chapter))
|
|
160
171
|
|
|
161
172
|
tree = ET.ElementTree(root)
|
|
162
173
|
ET.indent(tree, space=" ")
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import base64
|
|
7
7
|
import html
|
|
8
|
+
from collections.abc import Callable
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import ClassVar
|
|
10
11
|
|
|
@@ -98,13 +99,23 @@ if weasyprint is not None:
|
|
|
98
99
|
"""
|
|
99
100
|
self._transport = transport
|
|
100
101
|
|
|
101
|
-
async def export(
|
|
102
|
+
async def export(
|
|
103
|
+
self,
|
|
104
|
+
title: Title,
|
|
105
|
+
chapters: list[Chapter],
|
|
106
|
+
output_path: Path,
|
|
107
|
+
*,
|
|
108
|
+
on_chapter: Callable[[], None] | None = None,
|
|
109
|
+
) -> Path:
|
|
102
110
|
"""Write ``chapters`` to ``output_path`` as a PDF file.
|
|
103
111
|
|
|
104
112
|
Args:
|
|
105
113
|
title: The chapters' parent title; supplies metadata, cover, and authors.
|
|
106
114
|
chapters: The chapters to include, in the order they should appear.
|
|
107
115
|
output_path: Where to write the ``.pdf`` file.
|
|
116
|
+
on_chapter: Called once per chapter rendered to HTML, if given — after
|
|
117
|
+
illustrations have already been downloaded, see the ``Exporter``
|
|
118
|
+
protocol's docstring.
|
|
108
119
|
|
|
109
120
|
Returns:
|
|
110
121
|
``output_path``.
|
|
@@ -135,9 +146,12 @@ if weasyprint is not None:
|
|
|
135
146
|
for url, data in images.items()
|
|
136
147
|
}
|
|
137
148
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
chapter_html_parts = []
|
|
150
|
+
for chapter in chapters:
|
|
151
|
+
chapter_html_parts.append(_chapter_html(chapter, url_to_data_uri))
|
|
152
|
+
if on_chapter is not None:
|
|
153
|
+
on_chapter()
|
|
154
|
+
body = _title_page_html(title, cover_data_uri) + "".join(chapter_html_parts)
|
|
141
155
|
document = (
|
|
142
156
|
f"<html><head><meta charset='utf-8'><style>{_PAGE_CSS}</style></head>"
|
|
143
157
|
f"<body>{body}</body></html>"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from html.parser import HTMLParser
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import ClassVar
|
|
@@ -69,13 +70,21 @@ class TxtExporter:
|
|
|
69
70
|
|
|
70
71
|
format: ClassVar[str] = "txt"
|
|
71
72
|
|
|
72
|
-
async def export(
|
|
73
|
+
async def export(
|
|
74
|
+
self,
|
|
75
|
+
title: Title,
|
|
76
|
+
chapters: list[Chapter],
|
|
77
|
+
output_path: Path,
|
|
78
|
+
*,
|
|
79
|
+
on_chapter: Callable[[], None] | None = None,
|
|
80
|
+
) -> Path:
|
|
73
81
|
"""Write ``chapters`` to ``output_path`` as plain text.
|
|
74
82
|
|
|
75
83
|
Args:
|
|
76
84
|
title: The chapters' parent title; only its name is used.
|
|
77
85
|
chapters: The chapters to include, in the order they should appear.
|
|
78
86
|
output_path: Where to write the ``.txt`` file.
|
|
87
|
+
on_chapter: Called once per chapter written, if given.
|
|
79
88
|
|
|
80
89
|
Returns:
|
|
81
90
|
``output_path``.
|
|
@@ -84,6 +93,8 @@ class TxtExporter:
|
|
|
84
93
|
for chapter in chapters:
|
|
85
94
|
body = html_to_text(chapter.content or "")
|
|
86
95
|
sections.append(f"{chapter_heading(chapter)}\n\n{body}")
|
|
96
|
+
if on_chapter is not None:
|
|
97
|
+
on_chapter()
|
|
87
98
|
|
|
88
99
|
output_path.write_text("\n\n\n".join(sections) + "\n", encoding="utf-8")
|
|
89
100
|
return output_path
|