ranobelib-python-sdk 0.2.0__tar.gz → 0.3.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.3.0/PKG-INFO +112 -0
- ranobelib_python_sdk-0.3.0/README.md +84 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/pyproject.toml +2 -1
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/pyproject.toml.orig +2 -1
- 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.3.0}/src/ranobelib/__init__.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/cache.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/client.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exceptions.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/__init__.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/_illustrations.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/_shared.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/epub.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/fb2.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/pdf.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/txt.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/models.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/numbering.py +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/py.typed +0 -0
- {ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/sdk.py +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ranobelib-python-sdk
|
|
3
|
+
Version: 0.3.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: weasyprint>=69.0
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Project-URL: Homepage, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk
|
|
24
|
+
Project-URL: Repository, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk
|
|
25
|
+
Project-URL: Issues, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/issues
|
|
26
|
+
Project-URL: Changelog, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/blob/main/CHANGELOG.md
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# ranobelib-python-sdk
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/ranobelib-python-sdk/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](pyproject.toml)
|
|
34
|
+
[](https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/actions/workflows/ci.yml)
|
|
35
|
+
[](https://coveralls.io/github/LLIJIYAPNIK/ranobelib-python-sdk?branch=main)
|
|
36
|
+
[](https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/actions/workflows/docs.yml)
|
|
37
|
+
|
|
38
|
+
Async Python SDK for [ranobelib.me](https://ranobelib.me): fetch title metadata, download
|
|
39
|
+
chapters and volumes, and export to epub, fb2, txt, or pdf.
|
|
40
|
+
|
|
41
|
+
The SDK talks to the undocumented but open JSON API behind the site (`api.cdnlibs.org`,
|
|
42
|
+
part of the lib.social network) instead of scraping HTML.
|
|
43
|
+
|
|
44
|
+
> **Status:** all the calls in the quickstart below are implemented — metadata, table of
|
|
45
|
+
> contents, single/bulk chapter and volume downloads, whole-title bulk download, translation
|
|
46
|
+
> selection, and export to
|
|
47
|
+
> `"txt"`, `"fb2"`, `"epub"`, or `"pdf"` (cover + in-chapter illustrations embedded in
|
|
48
|
+
> epub/pdf). Raw API responses are cached to disk, and the underlying client bounds
|
|
49
|
+
> concurrency, paces requests, and retries 429/5xx with backoff. `fmt="pdf"` needs
|
|
50
|
+
> WeasyPrint's native dependencies (Pango/cairo/GTK3) installed on the system — `pip
|
|
51
|
+
> install`/`uv add` alone doesn't guarantee that on every platform; without them,
|
|
52
|
+
> `export(fmt="pdf")` raises the same "unknown format" error the other three formats don't.
|
|
53
|
+
> Authorization (private/18+/paid content) is out of scope — see `CLAUDE.md` for the full
|
|
54
|
+
> architecture and design decisions behind all of this.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv add ranobelib-python-sdk
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
or
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install ranobelib-python-sdk
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Requires Python 3.11+.
|
|
69
|
+
|
|
70
|
+
## Quickstart
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from ranobelib import RanobeLib
|
|
74
|
+
|
|
75
|
+
async with RanobeLib("https://ranobelib.me/ru/book/6712--high-school-dxd-novel") as lib:
|
|
76
|
+
info = await lib.get_info()
|
|
77
|
+
toc = await lib.get_table_of_contents()
|
|
78
|
+
|
|
79
|
+
chapter = await lib.get_chapter(volume=6, number="51.6")
|
|
80
|
+
volume = await lib.get_volume(volume=6)
|
|
81
|
+
|
|
82
|
+
chapters = await lib.get_chapters([(6, "51.6"), (6, "52")])
|
|
83
|
+
volumes = await lib.get_volumes([1, 2, 3])
|
|
84
|
+
|
|
85
|
+
translations = await lib.get_translations(volume=6, number="51.6")
|
|
86
|
+
chapter = await lib.get_chapter(volume=6, number="51.6", branch_id=...)
|
|
87
|
+
|
|
88
|
+
all_volumes = await lib.download_title(translation_index=0, chapter_delay=0.5)
|
|
89
|
+
|
|
90
|
+
await lib.export(chapters, fmt="epub", path="output.epub")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This is the full current public API — everything above is implemented and works today.
|
|
94
|
+
|
|
95
|
+
## Examples
|
|
96
|
+
|
|
97
|
+
Complete, runnable scripts for every feature above — title metadata, table of contents,
|
|
98
|
+
single/bulk chapter and volume downloads, translation selection, whole-title bulk download,
|
|
99
|
+
disk caching, export to all four formats, and error handling — live in
|
|
100
|
+
**[`examples/`](examples/)**, one heavily commented file per feature. Start there for
|
|
101
|
+
copy-pasteable code.
|
|
102
|
+
|
|
103
|
+
## Documentation
|
|
104
|
+
|
|
105
|
+
Full API reference and guides: **<https://LLIJIYAPNIK.github.io/ranobelib-python-sdk/>**
|
|
106
|
+
(built with `mkdocs-material` + `mkdocstrings`, deployed on push to `main`). `CLAUDE.md`
|
|
107
|
+
documents the full architecture, public API contract, and the reasoning behind the SDK's
|
|
108
|
+
design decisions. [`CHANGELOG.md`](CHANGELOG.md) lists what changed in each release.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
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.3.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"
|
|
@@ -40,6 +40,7 @@ email = "kas4wot@yandex.ru"
|
|
|
40
40
|
Homepage = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk"
|
|
41
41
|
Repository = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk"
|
|
42
42
|
Issues = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/issues"
|
|
43
|
+
Changelog = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/blob/main/CHANGELOG.md"
|
|
43
44
|
|
|
44
45
|
[build-system]
|
|
45
46
|
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.3.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"
|
|
@@ -31,6 +31,7 @@ dependencies = [
|
|
|
31
31
|
Homepage = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk"
|
|
32
32
|
Repository = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk"
|
|
33
33
|
Issues = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/issues"
|
|
34
|
+
Changelog = "https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/blob/main/CHANGELOG.md"
|
|
34
35
|
|
|
35
36
|
[build-system]
|
|
36
37
|
requires = ["uv_build>=0.11.26,<0.12.0"]
|
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ranobelib-python-sdk
|
|
3
|
-
Version: 0.2.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: weasyprint>=69.0
|
|
22
|
-
Requires-Python: >=3.11
|
|
23
|
-
Project-URL: Homepage, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk
|
|
24
|
-
Project-URL: Repository, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk
|
|
25
|
-
Project-URL: Issues, https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/issues
|
|
26
|
-
Description-Content-Type: text/markdown
|
|
27
|
-
|
|
28
|
-
# ranobelib-python-sdk
|
|
29
|
-
|
|
30
|
-
[](https://pypi.org/project/ranobelib-python-sdk/)
|
|
31
|
-
[](LICENSE)
|
|
32
|
-
[](pyproject.toml)
|
|
33
|
-
[](https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/actions/workflows/ci.yml)
|
|
34
|
-
[](https://coveralls.io/github/LLIJIYAPNIK/ranobelib-python-sdk?branch=main)
|
|
35
|
-
[](https://github.com/LLIJIYAPNIK/ranobelib-python-sdk/actions/workflows/docs.yml)
|
|
36
|
-
|
|
37
|
-
Async Python SDK for [ranobelib.me](https://ranobelib.me): fetch title metadata, download
|
|
38
|
-
chapters and volumes, and export to epub, fb2, txt, or pdf.
|
|
39
|
-
|
|
40
|
-
The SDK talks to the undocumented but open JSON API behind the site (`api.cdnlibs.org`,
|
|
41
|
-
part of the lib.social network) instead of scraping HTML.
|
|
42
|
-
|
|
43
|
-
> **Status:** all the calls in the quickstart below are implemented — metadata, table of
|
|
44
|
-
> contents, single/bulk chapter and volume downloads, whole-title bulk download, translation
|
|
45
|
-
> selection, and export to
|
|
46
|
-
> `"txt"`, `"fb2"`, `"epub"`, or `"pdf"` (cover + in-chapter illustrations embedded in
|
|
47
|
-
> epub/pdf). Raw API responses are cached to disk, and the underlying client bounds
|
|
48
|
-
> concurrency, paces requests, and retries 429/5xx with backoff. `fmt="pdf"` needs
|
|
49
|
-
> WeasyPrint's native dependencies (Pango/cairo/GTK3) installed on the system — `pip
|
|
50
|
-
> install`/`uv add` alone doesn't guarantee that on every platform; without them,
|
|
51
|
-
> `export(fmt="pdf")` raises the same "unknown format" error the other three formats don't.
|
|
52
|
-
> Authorization (private/18+/paid content) is out of scope — see `CLAUDE.md` for the full
|
|
53
|
-
> architecture and design decisions behind all of this.
|
|
54
|
-
|
|
55
|
-
## Installation
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
uv add ranobelib-python-sdk
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
or
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
pip install ranobelib-python-sdk
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Requires Python 3.11+.
|
|
68
|
-
|
|
69
|
-
## Quickstart
|
|
70
|
-
|
|
71
|
-
```python
|
|
72
|
-
from ranobelib import RanobeLib
|
|
73
|
-
|
|
74
|
-
async with RanobeLib("https://ranobelib.me/ru/book/6712--high-school-dxd-novel") as lib:
|
|
75
|
-
info = await lib.get_info()
|
|
76
|
-
toc = await lib.get_table_of_contents()
|
|
77
|
-
|
|
78
|
-
chapter = await lib.get_chapter(volume=6, number="51.6")
|
|
79
|
-
volume = await lib.get_volume(volume=6)
|
|
80
|
-
|
|
81
|
-
chapters = await lib.get_chapters([(6, "51.6"), (6, "52")])
|
|
82
|
-
volumes = await lib.get_volumes([1, 2, 3])
|
|
83
|
-
|
|
84
|
-
translations = await lib.get_translations(volume=6, number="51.6")
|
|
85
|
-
chapter = await lib.get_chapter(volume=6, number="51.6", branch_id=...)
|
|
86
|
-
|
|
87
|
-
all_volumes = await lib.download_title(translation_index=0, chapter_delay=0.5)
|
|
88
|
-
|
|
89
|
-
await lib.export(chapters, fmt="epub", path="output.epub")
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
This is the full current public API — everything above is implemented and works today.
|
|
93
|
-
|
|
94
|
-
## Examples
|
|
95
|
-
|
|
96
|
-
Every snippet below is a complete, self-contained script — save it as `example.py` and run
|
|
97
|
-
`python example.py`. Each one really was run against the live site to produce the "Result"
|
|
98
|
-
shown; only chapter content is truncated with `[:200]`/`[:300]` for readability, everything
|
|
99
|
-
else is the real output verbatim.
|
|
100
|
-
|
|
101
|
-
### Title metadata
|
|
102
|
-
|
|
103
|
-
```python
|
|
104
|
-
import asyncio
|
|
105
|
-
|
|
106
|
-
from ranobelib import RanobeLib
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
async def main() -> None:
|
|
110
|
-
async with RanobeLib("https://ranobelib.me/ru/book/6712--high-school-dxd-novel") as lib:
|
|
111
|
-
info = await lib.get_info()
|
|
112
|
-
print(info.name)
|
|
113
|
-
print(info.rus_name)
|
|
114
|
-
print(info.status.label)
|
|
115
|
-
print(info.chapter_count)
|
|
116
|
-
print([genre.name for genre in info.genres[:5]])
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
asyncio.run(main())
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
**Result:**
|
|
123
|
-
|
|
124
|
-
```
|
|
125
|
-
Haisukuru Di Di (Novel)
|
|
126
|
-
Старшая школа D×D (Новелла)
|
|
127
|
-
Завершён
|
|
128
|
-
308
|
|
129
|
-
['Боевик', 'Боевые искусства', 'Вампиры', 'Гарем', 'Драма']
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Table of contents
|
|
133
|
-
|
|
134
|
-
```python
|
|
135
|
-
import asyncio
|
|
136
|
-
|
|
137
|
-
from ranobelib import RanobeLib
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
async def main() -> None:
|
|
141
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
142
|
-
volumes = await lib.get_table_of_contents()
|
|
143
|
-
for volume in volumes:
|
|
144
|
-
print(f"Volume {volume.number}: {len(volume.chapters)} chapters")
|
|
145
|
-
print(volumes[0].chapters[0].name)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
asyncio.run(main())
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
**Result:**
|
|
152
|
-
|
|
153
|
-
```
|
|
154
|
-
Volume 0: 1 chapters
|
|
155
|
-
Volume 1: 46 chapters
|
|
156
|
-
(НЕ ГОТОВО) Возможный гарем
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### A single chapter
|
|
160
|
-
|
|
161
|
-
```python
|
|
162
|
-
import asyncio
|
|
163
|
-
|
|
164
|
-
from ranobelib import RanobeLib
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
async def main() -> None:
|
|
168
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
169
|
-
chapter = await lib.get_chapter(volume=1, number="1")
|
|
170
|
-
print(chapter.name)
|
|
171
|
-
print(chapter.content[:200])
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
asyncio.run(main())
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
**Result:**
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
Глава 1
|
|
181
|
-
<p data-paragraph-index="1">"Наконец, я наконец-то могу вернуться." - говорю я с широкой
|
|
182
|
-
улыбкой на лице, хотя по моим щекам текут слезы. Прошло слишком много времени с тех пор,
|
|
183
|
-
как я застрял в этом м
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Several chapters at once
|
|
187
|
-
|
|
188
|
-
```python
|
|
189
|
-
import asyncio
|
|
190
|
-
|
|
191
|
-
from ranobelib import RanobeLib
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
async def main() -> None:
|
|
195
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
196
|
-
chapters = await lib.get_chapters([(1, "1"), (1, "2")])
|
|
197
|
-
for chapter in chapters:
|
|
198
|
-
print(chapter.volume, chapter.number, len(chapter.content))
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
asyncio.run(main())
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
**Result:**
|
|
205
|
-
|
|
206
|
-
```
|
|
207
|
-
1 1 30722
|
|
208
|
-
1 2 22747
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### A whole volume
|
|
212
|
-
|
|
213
|
-
```python
|
|
214
|
-
import asyncio
|
|
215
|
-
|
|
216
|
-
from ranobelib import RanobeLib
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
async def main() -> None:
|
|
220
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
221
|
-
volume = await lib.get_volume(0)
|
|
222
|
-
print(volume.number, [chapter.number for chapter in volume.chapters])
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
asyncio.run(main())
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
**Result:**
|
|
229
|
-
|
|
230
|
-
```
|
|
231
|
-
0 ['1']
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### Several volumes at once
|
|
235
|
-
|
|
236
|
-
```python
|
|
237
|
-
import asyncio
|
|
238
|
-
|
|
239
|
-
from ranobelib import RanobeLib
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
async def main() -> None:
|
|
243
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
244
|
-
volumes = await lib.get_volumes([0, 1])
|
|
245
|
-
for volume in volumes:
|
|
246
|
-
print(volume.number, len(volume.chapters))
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
asyncio.run(main())
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
**Result:**
|
|
253
|
-
|
|
254
|
-
```
|
|
255
|
-
0 1
|
|
256
|
-
1 46
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### Selecting a translation
|
|
260
|
-
|
|
261
|
-
`11407--solo-leveling` has two competing translations of its prologue. `get_chapter()`
|
|
262
|
-
refuses to guess which one you want — it raises `MultipleTranslationsError` instead —
|
|
263
|
-
unless `branch_id` is given explicitly, so you list the options with `get_translations()`
|
|
264
|
-
first:
|
|
265
|
-
|
|
266
|
-
```python
|
|
267
|
-
import asyncio
|
|
268
|
-
|
|
269
|
-
from ranobelib import MultipleTranslationsError, RanobeLib
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
async def main() -> None:
|
|
273
|
-
async with RanobeLib("https://ranobelib.me/ru/book/11407--solo-leveling") as lib:
|
|
274
|
-
branches = await lib.get_translations(volume=1, number="0")
|
|
275
|
-
for branch in branches:
|
|
276
|
-
print(branch.branch_id, [team.name for team in branch.teams])
|
|
277
|
-
|
|
278
|
-
try:
|
|
279
|
-
await lib.get_chapter(volume=1, number="0")
|
|
280
|
-
except MultipleTranslationsError as exc:
|
|
281
|
-
print("ambiguous, pick one:", [b.branch_id for b in exc.branches])
|
|
282
|
-
|
|
283
|
-
chapter = await lib.get_chapter(volume=1, number="0", branch_id=branches[0].branch_id)
|
|
284
|
-
print(chapter.content[:60])
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
asyncio.run(main())
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**Result:**
|
|
291
|
-
|
|
292
|
-
```
|
|
293
|
-
2251 ['BerkuD13']
|
|
294
|
-
635 ['Неизвестный']
|
|
295
|
-
ambiguous, pick one: [2251, 635]
|
|
296
|
-
<p data-paragraph-index="1">Прокачка уровня в одиночку</p><p data-paragraph-index="2">0 . Пролог</p
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### Downloading a whole title
|
|
300
|
-
|
|
301
|
-
`download_title()` fetches every chapter of a title in one call. When a title has chapters
|
|
302
|
-
with more than one translation, it raises `MultipleTitleTranslationsError` listing *all* of
|
|
303
|
-
them up front (not just the first one hit) — pass `branch_id` or `translation_index` to
|
|
304
|
-
resolve them:
|
|
305
|
-
|
|
306
|
-
```python
|
|
307
|
-
import asyncio
|
|
308
|
-
|
|
309
|
-
from ranobelib import MultipleTitleTranslationsError, RanobeLib
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
async def main() -> None:
|
|
313
|
-
async with RanobeLib("https://ranobelib.me/ru/book/40195--enbizaka-no-shitateya") as lib:
|
|
314
|
-
volumes = await lib.download_title(chapter_delay=0.5)
|
|
315
|
-
for volume in volumes:
|
|
316
|
-
print(volume.number, [chapter.number for chapter in volume.chapters])
|
|
317
|
-
|
|
318
|
-
slug = "113306--bungou-stray-dogs-gaiden-ayatsuji-yukito-vs-kyogoku-natsuhiko"
|
|
319
|
-
async with RanobeLib(f"https://ranobelib.me/ru/book/{slug}") as lib:
|
|
320
|
-
try:
|
|
321
|
-
await lib.download_title()
|
|
322
|
-
except MultipleTitleTranslationsError as exc:
|
|
323
|
-
print(f"{len(exc.chapters)} ambiguous chapter(s):")
|
|
324
|
-
for chapter in exc.chapters:
|
|
325
|
-
print(" ", chapter.volume, chapter.number, [b.branch_id for b in chapter.branches])
|
|
326
|
-
|
|
327
|
-
volumes = await lib.download_title(translation_index=0)
|
|
328
|
-
print([chapter.number for chapter in volumes[0].chapters])
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
asyncio.run(main())
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
**Result:**
|
|
335
|
-
|
|
336
|
-
```
|
|
337
|
-
1 ['0', '1']
|
|
338
|
-
3 ambiguous chapter(s):
|
|
339
|
-
1 0 [12954, 12955]
|
|
340
|
-
1 1 [12955, 12954]
|
|
341
|
-
1 2 [12954, 12955]
|
|
342
|
-
['0', '1', '2', '3', '4', '5']
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
Note chapter `1`'s branches are listed in the opposite order from chapters `0`/`2` —
|
|
346
|
-
`translation_index=0` resolves by each chapter's own branch order, not a shared `branch_id`,
|
|
347
|
-
which is why it picks a different team for chapter `1` than for `0`/`2` here. Use `branch_id`
|
|
348
|
-
instead when you know the specific translation you want and it's shared across chapters.
|
|
349
|
-
|
|
350
|
-
### Disk caching
|
|
351
|
-
|
|
352
|
-
Raw API responses are cached to disk by default, so a repeated call for the same data
|
|
353
|
-
doesn't re-fetch it:
|
|
354
|
-
|
|
355
|
-
```python
|
|
356
|
-
import asyncio
|
|
357
|
-
import time
|
|
358
|
-
|
|
359
|
-
from ranobelib import RanobeLib
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
async def main() -> None:
|
|
363
|
-
async with RanobeLib(
|
|
364
|
-
"https://ranobelib.me/ru/book/91443--new-hero-in-dxd", cache_dir=".ranobelib_cache"
|
|
365
|
-
) as lib:
|
|
366
|
-
start = time.perf_counter()
|
|
367
|
-
await lib.get_table_of_contents()
|
|
368
|
-
print(f"first call: {time.perf_counter() - start:.3f}s (hits the API)")
|
|
369
|
-
|
|
370
|
-
start = time.perf_counter()
|
|
371
|
-
await lib.get_table_of_contents()
|
|
372
|
-
print(f"second call: {time.perf_counter() - start:.3f}s (served from disk cache)")
|
|
373
|
-
|
|
374
|
-
await lib.get_table_of_contents(refresh=True) # Bypasses the cache explicitly.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
asyncio.run(main())
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
**Result** (your exact timings will vary, but the gap between the two calls won't):
|
|
381
|
-
|
|
382
|
-
```
|
|
383
|
-
first call: 0.318s (hits the API)
|
|
384
|
-
second call: 0.001s (served from disk cache)
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### Exporting to txt / fb2 / epub / pdf
|
|
388
|
-
|
|
389
|
-
```python
|
|
390
|
-
import asyncio
|
|
391
|
-
|
|
392
|
-
from ranobelib import RanobeLib
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
async def main() -> None:
|
|
396
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
397
|
-
chapters = await lib.get_chapters([(1, "1"), (1, "2")])
|
|
398
|
-
for fmt in ("txt", "fb2", "epub", "pdf"):
|
|
399
|
-
path = f"output.{fmt}"
|
|
400
|
-
result = await lib.export(chapters, fmt=fmt, path=path)
|
|
401
|
-
print(fmt, "->", result)
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
asyncio.run(main())
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
**Result** (file sizes from the real run — `pdf` needs
|
|
408
|
-
[WeasyPrint's native dependencies](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation)
|
|
409
|
-
installed; without them this raises `ValueError: Unknown export format 'pdf'. Available:
|
|
410
|
-
epub, fb2, txt` instead of the line shown here):
|
|
411
|
-
|
|
412
|
-
```
|
|
413
|
-
txt -> output.txt (84,044 bytes)
|
|
414
|
-
fb2 -> output.fb2 (87,691 bytes)
|
|
415
|
-
epub -> output.epub (1,442,724 bytes, cover + illustrations embedded)
|
|
416
|
-
pdf -> output.pdf (cover + illustrations embedded, needs WeasyPrint)
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
`output.txt`'s first few lines, for a sense of the format:
|
|
420
|
-
|
|
421
|
-
```
|
|
422
|
-
New Hero in DxD
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
Volume 1, Chapter 1: Глава 1
|
|
426
|
-
|
|
427
|
-
"Наконец, я наконец-то могу вернуться." - говорю я с широкой улыбкой на лице, хотя по моим
|
|
428
|
-
щекам текут слезы. Прошло слишком много времени с тех пор, как я застрял в этом месте, я
|
|
429
|
-
больше не могу с этим справляться. Поначалу это было круто, но через нек...
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
### Error handling
|
|
433
|
-
|
|
434
|
-
```python
|
|
435
|
-
import asyncio
|
|
436
|
-
|
|
437
|
-
from ranobelib import ChapterNotFoundError, RanobeLib, TitleNotFoundError
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
async def main() -> None:
|
|
441
|
-
async with RanobeLib("https://ranobelib.me/ru/book/1--this-title-does-not-exist-zzz") as lib:
|
|
442
|
-
try:
|
|
443
|
-
await lib.get_info()
|
|
444
|
-
except TitleNotFoundError as exc:
|
|
445
|
-
print(exc)
|
|
446
|
-
|
|
447
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
448
|
-
try:
|
|
449
|
-
await lib.get_chapter(volume=999, number="9999")
|
|
450
|
-
except ChapterNotFoundError as exc:
|
|
451
|
-
print(exc)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
asyncio.run(main())
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
**Result:**
|
|
458
|
-
|
|
459
|
-
```
|
|
460
|
-
Title not found: '1--this-title-does-not-exist-zzz'
|
|
461
|
-
Chapter not found: '91443--new-hero-in-dxd' volume='999' number='9999'
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
`AuthRequiredError` (403, paid/early-access content) and `RateLimitError` (429 after
|
|
465
|
-
retries are exhausted) follow the same pattern — see the
|
|
466
|
-
[API reference](https://LLIJIYAPNIK.github.io/ranobelib-python-sdk/reference/) for every
|
|
467
|
-
exception's attributes.
|
|
468
|
-
|
|
469
|
-
## Documentation
|
|
470
|
-
|
|
471
|
-
Full API reference and guides: **<https://LLIJIYAPNIK.github.io/ranobelib-python-sdk/>**
|
|
472
|
-
(built with `mkdocs-material` + `mkdocstrings`, deployed on push to `main`). `CLAUDE.md`
|
|
473
|
-
documents the full architecture, public API contract, and the reasoning behind the SDK's
|
|
474
|
-
design decisions.
|
|
475
|
-
|
|
476
|
-
## License
|
|
477
|
-
|
|
478
|
-
MIT — see [LICENSE](LICENSE).
|
|
@@ -1,451 +0,0 @@
|
|
|
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
|
-
Every snippet below is a complete, self-contained script — save it as `example.py` and run
|
|
70
|
-
`python example.py`. Each one really was run against the live site to produce the "Result"
|
|
71
|
-
shown; only chapter content is truncated with `[:200]`/`[:300]` for readability, everything
|
|
72
|
-
else is the real output verbatim.
|
|
73
|
-
|
|
74
|
-
### Title metadata
|
|
75
|
-
|
|
76
|
-
```python
|
|
77
|
-
import asyncio
|
|
78
|
-
|
|
79
|
-
from ranobelib import RanobeLib
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
async def main() -> None:
|
|
83
|
-
async with RanobeLib("https://ranobelib.me/ru/book/6712--high-school-dxd-novel") as lib:
|
|
84
|
-
info = await lib.get_info()
|
|
85
|
-
print(info.name)
|
|
86
|
-
print(info.rus_name)
|
|
87
|
-
print(info.status.label)
|
|
88
|
-
print(info.chapter_count)
|
|
89
|
-
print([genre.name for genre in info.genres[:5]])
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
asyncio.run(main())
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**Result:**
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
Haisukuru Di Di (Novel)
|
|
99
|
-
Старшая школа D×D (Новелла)
|
|
100
|
-
Завершён
|
|
101
|
-
308
|
|
102
|
-
['Боевик', 'Боевые искусства', 'Вампиры', 'Гарем', 'Драма']
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### Table of contents
|
|
106
|
-
|
|
107
|
-
```python
|
|
108
|
-
import asyncio
|
|
109
|
-
|
|
110
|
-
from ranobelib import RanobeLib
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
async def main() -> None:
|
|
114
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
115
|
-
volumes = await lib.get_table_of_contents()
|
|
116
|
-
for volume in volumes:
|
|
117
|
-
print(f"Volume {volume.number}: {len(volume.chapters)} chapters")
|
|
118
|
-
print(volumes[0].chapters[0].name)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
asyncio.run(main())
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Result:**
|
|
125
|
-
|
|
126
|
-
```
|
|
127
|
-
Volume 0: 1 chapters
|
|
128
|
-
Volume 1: 46 chapters
|
|
129
|
-
(НЕ ГОТОВО) Возможный гарем
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### A single chapter
|
|
133
|
-
|
|
134
|
-
```python
|
|
135
|
-
import asyncio
|
|
136
|
-
|
|
137
|
-
from ranobelib import RanobeLib
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
async def main() -> None:
|
|
141
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
142
|
-
chapter = await lib.get_chapter(volume=1, number="1")
|
|
143
|
-
print(chapter.name)
|
|
144
|
-
print(chapter.content[:200])
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
asyncio.run(main())
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
**Result:**
|
|
151
|
-
|
|
152
|
-
```
|
|
153
|
-
Глава 1
|
|
154
|
-
<p data-paragraph-index="1">"Наконец, я наконец-то могу вернуться." - говорю я с широкой
|
|
155
|
-
улыбкой на лице, хотя по моим щекам текут слезы. Прошло слишком много времени с тех пор,
|
|
156
|
-
как я застрял в этом м
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Several chapters at once
|
|
160
|
-
|
|
161
|
-
```python
|
|
162
|
-
import asyncio
|
|
163
|
-
|
|
164
|
-
from ranobelib import RanobeLib
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
async def main() -> None:
|
|
168
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
169
|
-
chapters = await lib.get_chapters([(1, "1"), (1, "2")])
|
|
170
|
-
for chapter in chapters:
|
|
171
|
-
print(chapter.volume, chapter.number, len(chapter.content))
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
asyncio.run(main())
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
**Result:**
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
1 1 30722
|
|
181
|
-
1 2 22747
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### A whole volume
|
|
185
|
-
|
|
186
|
-
```python
|
|
187
|
-
import asyncio
|
|
188
|
-
|
|
189
|
-
from ranobelib import RanobeLib
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
async def main() -> None:
|
|
193
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
194
|
-
volume = await lib.get_volume(0)
|
|
195
|
-
print(volume.number, [chapter.number for chapter in volume.chapters])
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
asyncio.run(main())
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**Result:**
|
|
202
|
-
|
|
203
|
-
```
|
|
204
|
-
0 ['1']
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Several volumes at once
|
|
208
|
-
|
|
209
|
-
```python
|
|
210
|
-
import asyncio
|
|
211
|
-
|
|
212
|
-
from ranobelib import RanobeLib
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
async def main() -> None:
|
|
216
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
217
|
-
volumes = await lib.get_volumes([0, 1])
|
|
218
|
-
for volume in volumes:
|
|
219
|
-
print(volume.number, len(volume.chapters))
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
asyncio.run(main())
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
**Result:**
|
|
226
|
-
|
|
227
|
-
```
|
|
228
|
-
0 1
|
|
229
|
-
1 46
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### Selecting a translation
|
|
233
|
-
|
|
234
|
-
`11407--solo-leveling` has two competing translations of its prologue. `get_chapter()`
|
|
235
|
-
refuses to guess which one you want — it raises `MultipleTranslationsError` instead —
|
|
236
|
-
unless `branch_id` is given explicitly, so you list the options with `get_translations()`
|
|
237
|
-
first:
|
|
238
|
-
|
|
239
|
-
```python
|
|
240
|
-
import asyncio
|
|
241
|
-
|
|
242
|
-
from ranobelib import MultipleTranslationsError, RanobeLib
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
async def main() -> None:
|
|
246
|
-
async with RanobeLib("https://ranobelib.me/ru/book/11407--solo-leveling") as lib:
|
|
247
|
-
branches = await lib.get_translations(volume=1, number="0")
|
|
248
|
-
for branch in branches:
|
|
249
|
-
print(branch.branch_id, [team.name for team in branch.teams])
|
|
250
|
-
|
|
251
|
-
try:
|
|
252
|
-
await lib.get_chapter(volume=1, number="0")
|
|
253
|
-
except MultipleTranslationsError as exc:
|
|
254
|
-
print("ambiguous, pick one:", [b.branch_id for b in exc.branches])
|
|
255
|
-
|
|
256
|
-
chapter = await lib.get_chapter(volume=1, number="0", branch_id=branches[0].branch_id)
|
|
257
|
-
print(chapter.content[:60])
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
asyncio.run(main())
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
**Result:**
|
|
264
|
-
|
|
265
|
-
```
|
|
266
|
-
2251 ['BerkuD13']
|
|
267
|
-
635 ['Неизвестный']
|
|
268
|
-
ambiguous, pick one: [2251, 635]
|
|
269
|
-
<p data-paragraph-index="1">Прокачка уровня в одиночку</p><p data-paragraph-index="2">0 . Пролог</p
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Downloading a whole title
|
|
273
|
-
|
|
274
|
-
`download_title()` fetches every chapter of a title in one call. When a title has chapters
|
|
275
|
-
with more than one translation, it raises `MultipleTitleTranslationsError` listing *all* of
|
|
276
|
-
them up front (not just the first one hit) — pass `branch_id` or `translation_index` to
|
|
277
|
-
resolve them:
|
|
278
|
-
|
|
279
|
-
```python
|
|
280
|
-
import asyncio
|
|
281
|
-
|
|
282
|
-
from ranobelib import MultipleTitleTranslationsError, RanobeLib
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
async def main() -> None:
|
|
286
|
-
async with RanobeLib("https://ranobelib.me/ru/book/40195--enbizaka-no-shitateya") as lib:
|
|
287
|
-
volumes = await lib.download_title(chapter_delay=0.5)
|
|
288
|
-
for volume in volumes:
|
|
289
|
-
print(volume.number, [chapter.number for chapter in volume.chapters])
|
|
290
|
-
|
|
291
|
-
slug = "113306--bungou-stray-dogs-gaiden-ayatsuji-yukito-vs-kyogoku-natsuhiko"
|
|
292
|
-
async with RanobeLib(f"https://ranobelib.me/ru/book/{slug}") as lib:
|
|
293
|
-
try:
|
|
294
|
-
await lib.download_title()
|
|
295
|
-
except MultipleTitleTranslationsError as exc:
|
|
296
|
-
print(f"{len(exc.chapters)} ambiguous chapter(s):")
|
|
297
|
-
for chapter in exc.chapters:
|
|
298
|
-
print(" ", chapter.volume, chapter.number, [b.branch_id for b in chapter.branches])
|
|
299
|
-
|
|
300
|
-
volumes = await lib.download_title(translation_index=0)
|
|
301
|
-
print([chapter.number for chapter in volumes[0].chapters])
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
asyncio.run(main())
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
**Result:**
|
|
308
|
-
|
|
309
|
-
```
|
|
310
|
-
1 ['0', '1']
|
|
311
|
-
3 ambiguous chapter(s):
|
|
312
|
-
1 0 [12954, 12955]
|
|
313
|
-
1 1 [12955, 12954]
|
|
314
|
-
1 2 [12954, 12955]
|
|
315
|
-
['0', '1', '2', '3', '4', '5']
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
Note chapter `1`'s branches are listed in the opposite order from chapters `0`/`2` —
|
|
319
|
-
`translation_index=0` resolves by each chapter's own branch order, not a shared `branch_id`,
|
|
320
|
-
which is why it picks a different team for chapter `1` than for `0`/`2` here. Use `branch_id`
|
|
321
|
-
instead when you know the specific translation you want and it's shared across chapters.
|
|
322
|
-
|
|
323
|
-
### Disk caching
|
|
324
|
-
|
|
325
|
-
Raw API responses are cached to disk by default, so a repeated call for the same data
|
|
326
|
-
doesn't re-fetch it:
|
|
327
|
-
|
|
328
|
-
```python
|
|
329
|
-
import asyncio
|
|
330
|
-
import time
|
|
331
|
-
|
|
332
|
-
from ranobelib import RanobeLib
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
async def main() -> None:
|
|
336
|
-
async with RanobeLib(
|
|
337
|
-
"https://ranobelib.me/ru/book/91443--new-hero-in-dxd", cache_dir=".ranobelib_cache"
|
|
338
|
-
) as lib:
|
|
339
|
-
start = time.perf_counter()
|
|
340
|
-
await lib.get_table_of_contents()
|
|
341
|
-
print(f"first call: {time.perf_counter() - start:.3f}s (hits the API)")
|
|
342
|
-
|
|
343
|
-
start = time.perf_counter()
|
|
344
|
-
await lib.get_table_of_contents()
|
|
345
|
-
print(f"second call: {time.perf_counter() - start:.3f}s (served from disk cache)")
|
|
346
|
-
|
|
347
|
-
await lib.get_table_of_contents(refresh=True) # Bypasses the cache explicitly.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
asyncio.run(main())
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
**Result** (your exact timings will vary, but the gap between the two calls won't):
|
|
354
|
-
|
|
355
|
-
```
|
|
356
|
-
first call: 0.318s (hits the API)
|
|
357
|
-
second call: 0.001s (served from disk cache)
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### Exporting to txt / fb2 / epub / pdf
|
|
361
|
-
|
|
362
|
-
```python
|
|
363
|
-
import asyncio
|
|
364
|
-
|
|
365
|
-
from ranobelib import RanobeLib
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
async def main() -> None:
|
|
369
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
370
|
-
chapters = await lib.get_chapters([(1, "1"), (1, "2")])
|
|
371
|
-
for fmt in ("txt", "fb2", "epub", "pdf"):
|
|
372
|
-
path = f"output.{fmt}"
|
|
373
|
-
result = await lib.export(chapters, fmt=fmt, path=path)
|
|
374
|
-
print(fmt, "->", result)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
asyncio.run(main())
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
**Result** (file sizes from the real run — `pdf` needs
|
|
381
|
-
[WeasyPrint's native dependencies](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation)
|
|
382
|
-
installed; without them this raises `ValueError: Unknown export format 'pdf'. Available:
|
|
383
|
-
epub, fb2, txt` instead of the line shown here):
|
|
384
|
-
|
|
385
|
-
```
|
|
386
|
-
txt -> output.txt (84,044 bytes)
|
|
387
|
-
fb2 -> output.fb2 (87,691 bytes)
|
|
388
|
-
epub -> output.epub (1,442,724 bytes, cover + illustrations embedded)
|
|
389
|
-
pdf -> output.pdf (cover + illustrations embedded, needs WeasyPrint)
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
`output.txt`'s first few lines, for a sense of the format:
|
|
393
|
-
|
|
394
|
-
```
|
|
395
|
-
New Hero in DxD
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
Volume 1, Chapter 1: Глава 1
|
|
399
|
-
|
|
400
|
-
"Наконец, я наконец-то могу вернуться." - говорю я с широкой улыбкой на лице, хотя по моим
|
|
401
|
-
щекам текут слезы. Прошло слишком много времени с тех пор, как я застрял в этом месте, я
|
|
402
|
-
больше не могу с этим справляться. Поначалу это было круто, но через нек...
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
### Error handling
|
|
406
|
-
|
|
407
|
-
```python
|
|
408
|
-
import asyncio
|
|
409
|
-
|
|
410
|
-
from ranobelib import ChapterNotFoundError, RanobeLib, TitleNotFoundError
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
async def main() -> None:
|
|
414
|
-
async with RanobeLib("https://ranobelib.me/ru/book/1--this-title-does-not-exist-zzz") as lib:
|
|
415
|
-
try:
|
|
416
|
-
await lib.get_info()
|
|
417
|
-
except TitleNotFoundError as exc:
|
|
418
|
-
print(exc)
|
|
419
|
-
|
|
420
|
-
async with RanobeLib("https://ranobelib.me/ru/book/91443--new-hero-in-dxd") as lib:
|
|
421
|
-
try:
|
|
422
|
-
await lib.get_chapter(volume=999, number="9999")
|
|
423
|
-
except ChapterNotFoundError as exc:
|
|
424
|
-
print(exc)
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
asyncio.run(main())
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
**Result:**
|
|
431
|
-
|
|
432
|
-
```
|
|
433
|
-
Title not found: '1--this-title-does-not-exist-zzz'
|
|
434
|
-
Chapter not found: '91443--new-hero-in-dxd' volume='999' number='9999'
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
`AuthRequiredError` (403, paid/early-access content) and `RateLimitError` (429 after
|
|
438
|
-
retries are exhausted) follow the same pattern — see the
|
|
439
|
-
[API reference](https://LLIJIYAPNIK.github.io/ranobelib-python-sdk/reference/) for every
|
|
440
|
-
exception's attributes.
|
|
441
|
-
|
|
442
|
-
## Documentation
|
|
443
|
-
|
|
444
|
-
Full API reference and guides: **<https://LLIJIYAPNIK.github.io/ranobelib-python-sdk/>**
|
|
445
|
-
(built with `mkdocs-material` + `mkdocstrings`, deployed on push to `main`). `CLAUDE.md`
|
|
446
|
-
documents the full architecture, public API contract, and the reasoning behind the SDK's
|
|
447
|
-
design decisions.
|
|
448
|
-
|
|
449
|
-
## License
|
|
450
|
-
|
|
451
|
-
MIT — see [LICENSE](LICENSE).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/__init__.py
RENAMED
|
File without changes
|
{ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/_illustrations.py
RENAMED
|
File without changes
|
{ranobelib_python_sdk-0.2.0 → ranobelib_python_sdk-0.3.0}/src/ranobelib/exporters/_shared.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|