Spaces:
Running
Running
Upload 6 files
Browse files- README.md +65 -14
- app.py +76 -0
- create_epub.py +385 -0
- get_ranobe_content.py +310 -0
- pipeline.py +50 -0
- requirements.txt +3 -0
README.md
CHANGED
@@ -1,14 +1,65 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Конвертер ранобэ с сайта ranobelib.me в EPUB
|
2 |
+
|
3 |
+
Попробовать в онлайне: [Демонстрация на Hugging Face Spaces](https://huggingface.co/spaces/рыба)
|
4 |
+
|
5 |
+
Данный проект позволяет автоматически собирать тома и главы ранобэ с сайта **ranobelib.me** и конвертировать их в удобный формат EPUB.
|
6 |
+
Все скачанные главы и изображения сохраняются в локальную папку `output`, а итоговый файл в формате EPUB также кладётся в эту же директорию (либо в поддиректорию, в зависимости от настроек).
|
7 |
+
|
8 |
+
## Особенности
|
9 |
+
- Поддержка Python от **3.9** до **3.11** (рекомендуется 3.10).
|
10 |
+
- Сохранение иллюстраций в высоком качестве.
|
11 |
+
- Создаётся структурированное оглавление.
|
12 |
+
- EPUB-файлы совместимы с большинством читалок (Calibre, FBReader и т.д.).
|
13 |
+
- Удобный web-интерфейс на базе **Gradio**.
|
14 |
+
|
15 |
+
## Установка и запуск
|
16 |
+
|
17 |
+
1. **Убедитесь, что установлен Python 3.9–3.11**
|
18 |
+
Проверить версию Python можно так:
|
19 |
+
```bash
|
20 |
+
python --version
|
21 |
+
```
|
22 |
+
Если у вас несколько версий Python, используйте `python3` или `py -3`, в зависимости от вашей ОС.
|
23 |
+
|
24 |
+
2. **Клонируйте репозиторий или скачайте архив с кодом**
|
25 |
+
```bash
|
26 |
+
git clone https://github.com/ВАШ_РЕПО/ranobe-epub-converter.git
|
27 |
+
cd ranobe-epub-converter
|
28 |
+
```
|
29 |
+
|
30 |
+
3. **Создайте и активируйте виртуальное окружение** (рекомендуется для изоляции зависимостей):
|
31 |
+
```bash
|
32 |
+
python -m venv venv
|
33 |
+
```
|
34 |
+
Активация виртуального окружения:
|
35 |
+
- **Windows**:
|
36 |
+
```bash
|
37 |
+
venv\Scripts\activate
|
38 |
+
```
|
39 |
+
- **Linux/Mac**:
|
40 |
+
```bash
|
41 |
+
source venv/bin/activate
|
42 |
+
```
|
43 |
+
|
44 |
+
4. **Установите зависимости**:
|
45 |
+
```bash
|
46 |
+
pip install -r requirements.txt
|
47 |
+
```
|
48 |
+
|
49 |
+
5. **Запустите приложение**:
|
50 |
+
```bash
|
51 |
+
python app.py
|
52 |
+
```
|
53 |
+
После этого в консоли появится адрес, по которому будет доступен web-интерфейс (обычно `http://127.0.0.1:7860`).
|
54 |
+
|
55 |
+
## Использование
|
56 |
+
|
57 |
+
1. Перейдите в браузере по адресу, который вы увидите в консоли (например, http://127.0.0.1:7860).
|
58 |
+
2. Вставьте ссылку на ранобэ с `ranobelib.me` в текстовое поле.
|
59 |
+
3. Нажмите «Получить EPUB» и дождитесь завершения.
|
60 |
+
4. Готовый EPUB-файл появится в поле «Выходные файлы» и будет также сохранён в директорию `output/<случайный_id>`.
|
61 |
+
|
62 |
+
## Где искать скачанные данные?
|
63 |
+
|
64 |
+
- **Папка `output`** в корне проекта — все загруженные изображения и главы сохраняются в ней.
|
65 |
+
- Для каждой новой конвертации создаётся отдельная подпапка по случайному UUID (например, `output/123e4567-e89b-12d3-a456-426614174000`), чтобы избежать конфликтов при множественных загрузках.
|
app.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from pipeline import run_pipeline
|
3 |
+
import os
|
4 |
+
import urllib3
|
5 |
+
from uuid import uuid4
|
6 |
+
|
7 |
+
def process_url(url):
|
8 |
+
try:
|
9 |
+
# Проверяем что это ranobelib.me
|
10 |
+
if not url.startswith("https://ranobelib.me"):
|
11 |
+
return None, "Ошибка: Принимаются только ссылки с ranobelib.me"
|
12 |
+
|
13 |
+
random_folder = str(uuid4())
|
14 |
+
# create output folder
|
15 |
+
output_dir = f'output/{random_folder}'
|
16 |
+
os.makedirs(output_dir, exist_ok=True)
|
17 |
+
|
18 |
+
output_path = run_pipeline(url, output_dir=output_dir,progress=gr.Progress())
|
19 |
+
|
20 |
+
# Если файл создан успешно, возвращаем его и сообщение в статус
|
21 |
+
if os.path.exists(output_path):
|
22 |
+
return output_path, f"EPUB создан успешно: {output_path}"
|
23 |
+
else:
|
24 |
+
return None, "Ошибка: Файл не был создан"
|
25 |
+
|
26 |
+
except Exception as e:
|
27 |
+
raise e
|
28 |
+
return None, f"Ошибка: {str(e)}"
|
29 |
+
|
30 |
+
# Создаем интерфейс
|
31 |
+
with gr.Blocks() as demo:
|
32 |
+
gr.Markdown("""
|
33 |
+
# Конвертер ранобэ с сайта ranobelib.me в EPUB
|
34 |
+
|
35 |
+
Удобный инструмент для создания электронных книг из любимых ранобэ. Программа автоматически соберет все тома и главы в единый EPUB-файл.
|
36 |
+
|
37 |
+
### Инструкция:
|
38 |
+
1. Скопируйте ссылку на ранобэ с сайта **ranobelib.me**
|
39 |
+
2. Вставьте её в поле ввода ниже
|
40 |
+
3. Нажмите кнопку "Получить Epub" и дождитесь завершения конвертации
|
41 |
+
|
42 |
+
### Пример ссылки:
|
43 |
+
```
|
44 |
+
https://ranobelib.me/ru/book/88265--kurasu-no-daikiraina-joshi-to-kekkon-suru-koto-ni-natta
|
45 |
+
```
|
46 |
+
|
47 |
+
### Особенности:
|
48 |
+
- Работает только с сайтом **ranobelib.me**
|
49 |
+
- Время конвертации зависит от размера произведения
|
50 |
+
- В готовый файл включаются:
|
51 |
+
- Структурированное оглавление
|
52 |
+
- Все иллюстрации в высоком качестве
|
53 |
+
- Текст в удобном для чтения формате
|
54 |
+
- EPUB-файл совместим со всеми современными читалками
|
55 |
+
""")
|
56 |
+
status_bar = gr.Label(label="Статус")
|
57 |
+
|
58 |
+
with gr.Row():
|
59 |
+
with gr.Column():
|
60 |
+
url_input = gr.Textbox(
|
61 |
+
label="URL ранобэ",
|
62 |
+
placeholder="Вставьте ссылку на ранобэ с Ranobelib.me"
|
63 |
+
)
|
64 |
+
with gr.Column():
|
65 |
+
output_files = gr.Files(label="Выходные файлы")
|
66 |
+
convert_btn = gr.Button("Получить Epub")
|
67 |
+
|
68 |
+
|
69 |
+
convert_btn.click(
|
70 |
+
fn=process_url,
|
71 |
+
inputs=url_input,
|
72 |
+
outputs=[output_files,status_bar]
|
73 |
+
)
|
74 |
+
|
75 |
+
if __name__ == "__main__":
|
76 |
+
demo.launch()
|
create_epub.py
ADDED
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
+
import io
|
5 |
+
from pathlib import Path
|
6 |
+
from ebooklib import epub
|
7 |
+
from collections import defaultdict
|
8 |
+
from bs4 import BeautifulSoup # для поиска <img src="imgs/...">
|
9 |
+
from PIL import Image
|
10 |
+
|
11 |
+
logging.basicConfig(
|
12 |
+
level=logging.INFO,
|
13 |
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
14 |
+
handlers=[logging.StreamHandler()]
|
15 |
+
)
|
16 |
+
|
17 |
+
class EpubCreator:
|
18 |
+
def __init__(self, ranobe_json_path, image_quality=85):
|
19 |
+
"""
|
20 |
+
:param ranobe_json_path: путь к ranobe.json
|
21 |
+
:param image_quality: качество JPEG, по умолч. 85
|
22 |
+
"""
|
23 |
+
self.ranobe_path = Path(ranobe_json_path)
|
24 |
+
if not self.ranobe_path.exists():
|
25 |
+
raise FileNotFoundError(f"Нет файла {ranobe_json_path}")
|
26 |
+
|
27 |
+
self.base_dir = self.ranobe_path.parent
|
28 |
+
self.image_quality = image_quality
|
29 |
+
self._image_cache = {} # кэш сжатых изображений
|
30 |
+
|
31 |
+
with open(self.ranobe_path, "r", encoding="utf-8") as f:
|
32 |
+
self.ranobe_data = json.load(f)
|
33 |
+
|
34 |
+
self.book = epub.EpubBook()
|
35 |
+
|
36 |
+
def create_epub(self):
|
37 |
+
"""
|
38 |
+
Создаём EPUB:
|
39 |
+
1) Обложка (при наличии),
|
40 |
+
2) Титульная страница,
|
41 |
+
3) Главы (группируем по томам),
|
42 |
+
4) Сохраняем.
|
43 |
+
"""
|
44 |
+
# Метаданные
|
45 |
+
self.book.set_identifier(f"ranobe_{self.ranobe_data['id']}")
|
46 |
+
self.book.set_title(self.ranobe_data['title'])
|
47 |
+
self.book.set_language("ru")
|
48 |
+
|
49 |
+
# CSS
|
50 |
+
style_item = self._create_style()
|
51 |
+
self.book.add_item(style_item)
|
52 |
+
|
53 |
+
spine = ["nav"]
|
54 |
+
toc = []
|
55 |
+
|
56 |
+
# Обложка
|
57 |
+
if self.ranobe_data.get("cover_image"):
|
58 |
+
cover_fullpath = self.base_dir / self.ranobe_data["cover_image"]
|
59 |
+
if cover_fullpath.exists():
|
60 |
+
try:
|
61 |
+
# Создаём страницу cover.xhtml
|
62 |
+
cover_page = epub.EpubHtml(
|
63 |
+
title="Cover",
|
64 |
+
file_name="cover.xhtml",
|
65 |
+
content='<div style="text-align:center;"><img src="images/cover.jpg" alt="cover" /></div>'
|
66 |
+
)
|
67 |
+
cover_page.add_item(style_item)
|
68 |
+
self.book.add_item(cover_page)
|
69 |
+
|
70 |
+
# Добавляем в spine
|
71 |
+
spine.insert(0, cover_page)
|
72 |
+
|
73 |
+
# Сжимаем и делаем set_cover
|
74 |
+
cov_data = self._compress_image(cover_fullpath)
|
75 |
+
self.book.set_cover("images/cover.jpg", cov_data)
|
76 |
+
except Exception as e:
|
77 |
+
logging.warning(f"Не удалось обработать обложку: {e}")
|
78 |
+
|
79 |
+
# Титульная страница
|
80 |
+
title_page = self._make_title_page(style_item)
|
81 |
+
self.book.add_item(title_page)
|
82 |
+
spine.append(title_page)
|
83 |
+
toc.append(title_page)
|
84 |
+
|
85 |
+
# Группируем главы по томам
|
86 |
+
volumes_map = defaultdict(list)
|
87 |
+
for ch in self.ranobe_data["chapters"]:
|
88 |
+
volumes_map[ch["volume"]].append(ch)
|
89 |
+
|
90 |
+
# Сортируем ключи "томов" как числа, но если вдруг не число - как строку
|
91 |
+
def _vol_key(v):
|
92 |
+
try:
|
93 |
+
return float(v)
|
94 |
+
except:
|
95 |
+
return v
|
96 |
+
|
97 |
+
sorted_vols = sorted(volumes_map.keys(), key=_vol_key)
|
98 |
+
|
99 |
+
volumes_for_toc = []
|
100 |
+
|
101 |
+
for vol in sorted_vols:
|
102 |
+
vol_title = f"Том {vol}"
|
103 |
+
vol_filename = f"volume_{vol}.xhtml"
|
104 |
+
vol_content_parts = [f'<h2 id="volume_{vol}">{vol_title}</h2>']
|
105 |
+
chapters_toc = []
|
106 |
+
|
107 |
+
# Сортируем главы по номеру
|
108 |
+
chapters = sorted(volumes_map[vol], key=lambda c: float(c["chapter"]))
|
109 |
+
for cinfo in chapters:
|
110 |
+
ch_id = cinfo["id"]
|
111 |
+
ch_title = f"Глава {cinfo['chapter']} - {cinfo['name']}"
|
112 |
+
anchor = f"chapter_{ch_id}"
|
113 |
+
|
114 |
+
vol_content_parts.append(f'<h3 id="{anchor}">{ch_title}</h3>')
|
115 |
+
|
116 |
+
# Обрабатываем контент
|
117 |
+
chapter_html = self._process_chapter_content(
|
118 |
+
cinfo["content"],
|
119 |
+
cinfo.get("attachments", [])
|
120 |
+
)
|
121 |
+
vol_content_parts.append(chapter_html)
|
122 |
+
|
123 |
+
chapters_toc.append((anchor, ch_title))
|
124 |
+
|
125 |
+
# Создаём EpubHtml для всего тома
|
126 |
+
vol_html = epub.EpubHtml(
|
127 |
+
title=vol_title,
|
128 |
+
file_name=vol_filename,
|
129 |
+
content="\n".join(vol_content_parts)
|
130 |
+
)
|
131 |
+
vol_html.add_item(style_item)
|
132 |
+
self.book.add_item(vol_html)
|
133 |
+
spine.append(vol_html)
|
134 |
+
|
135 |
+
volumes_for_toc.append((vol_title, vol_filename, chapters_toc))
|
136 |
+
|
137 |
+
# Формируем многоуровневое TOC
|
138 |
+
for (v_title, v_fname, chap_list) in volumes_for_toc:
|
139 |
+
vol_section = epub.Section(v_title, v_fname)
|
140 |
+
subitems = []
|
141 |
+
for (anchor, ch_title) in chap_list:
|
142 |
+
href = f"{v_fname}#{anchor}"
|
143 |
+
link_item = epub.Link(href, ch_title, f"chap_{anchor}")
|
144 |
+
subitems.append(link_item)
|
145 |
+
toc.append((vol_section, subitems))
|
146 |
+
|
147 |
+
self.book.toc = toc
|
148 |
+
self.book.spine = spine
|
149 |
+
self.book.add_item(epub.EpubNav())
|
150 |
+
self.book.add_item(epub.EpubNcx())
|
151 |
+
|
152 |
+
# Сохраняем
|
153 |
+
out_name = f"{self.ranobe_data['title']}.epub"
|
154 |
+
out_path = self.base_dir / out_name
|
155 |
+
epub.write_epub(str(out_path), self.book, {})
|
156 |
+
logging.info(f"EPUB создан: {out_path}")
|
157 |
+
return str(out_path)
|
158 |
+
|
159 |
+
def _process_chapter_content(self, content, attachments):
|
160 |
+
"""
|
161 |
+
content может быть:
|
162 |
+
- строка HTML
|
163 |
+
- объект типа {"type": "doc", ...} (ProseMirror-формат)
|
164 |
+
- что-то ещё (None и т.д.)
|
165 |
+
Если doc-формат, конвертируем в HTML через _doc_to_html.
|
166 |
+
Далее создаём BeautifulSoup, ищем <img src="imgs/...">,
|
167 |
+
сжимаем и добавляем в EPUB, подменяя src="images/...".
|
168 |
+
"""
|
169 |
+
# 1) Если контент - строка (HTML)
|
170 |
+
if isinstance(content, str):
|
171 |
+
raw_html = content
|
172 |
+
# 2) Если контент - dict (doc-формат)
|
173 |
+
elif isinstance(content, dict) and content.get("type") == "doc":
|
174 |
+
raw_html = self._doc_to_html(content, attachments)
|
175 |
+
else:
|
176 |
+
# не знаем, что это, вернём пустую строку
|
177 |
+
return ""
|
178 |
+
|
179 |
+
# Теперь обрабатываем получившийся HTML
|
180 |
+
soup = BeautifulSoup(raw_html, "html.parser")
|
181 |
+
all_imgs = soup.find_all("img")
|
182 |
+
for tag in all_imgs:
|
183 |
+
old_src = tag.get("src")
|
184 |
+
if not old_src:
|
185 |
+
continue
|
186 |
+
if old_src.startswith("imgs/"):
|
187 |
+
local_file = self.base_dir / old_src # "output/imgs/filename.jpg" и т.п.
|
188 |
+
if local_file.exists():
|
189 |
+
# Сжать + добавить
|
190 |
+
new_data = self._compress_image(local_file)
|
191 |
+
new_filename = "images/" + os.path.basename(local_file)
|
192 |
+
# Добавляем в книгу
|
193 |
+
item = epub.EpubItem(
|
194 |
+
uid=f"img_{os.path.basename(old_src)}",
|
195 |
+
file_name=new_filename,
|
196 |
+
media_type="image/jpeg",
|
197 |
+
content=new_data
|
198 |
+
)
|
199 |
+
self.book.add_item(item)
|
200 |
+
|
201 |
+
# Меняем src
|
202 |
+
tag["src"] = new_filename
|
203 |
+
else:
|
204 |
+
logging.warning(f"Файл {local_file} не найден, пропускаем.")
|
205 |
+
return str(soup)
|
206 |
+
|
207 |
+
def _doc_to_html(self, doc_content, attachments):
|
208 |
+
"""
|
209 |
+
Конвертация ProseMirror-формата (doc) в простой HTML.
|
210 |
+
attachments - список вложений, где filename соответствует "image".
|
211 |
+
|
212 |
+
Пример структуры:
|
213 |
+
{
|
214 |
+
"type": "doc",
|
215 |
+
"content": [
|
216 |
+
{"type": "paragraph", "content": [{"type": "text","text":"..."}]},
|
217 |
+
{"type": "image", "attrs": {"images": [{"image":"xxxx"}]}},
|
218 |
+
...
|
219 |
+
]
|
220 |
+
}
|
221 |
+
|
222 |
+
Нужно:
|
223 |
+
- paragraph -> <p>текст</p>
|
224 |
+
- image -> <img src="imgs/файл-из-attachments" />
|
225 |
+
- если встречаются другие типы, игнорируем или обрабатываем как абзац.
|
226 |
+
"""
|
227 |
+
if doc_content.get("type") != "doc":
|
228 |
+
return ""
|
229 |
+
|
230 |
+
content_arr = doc_content.get("content", [])
|
231 |
+
html_parts = []
|
232 |
+
|
233 |
+
# Для быстрого доступа: "имяБезРасширения" -> attachment["filename"]
|
234 |
+
# или просто сделаем словарь image_name -> filename
|
235 |
+
name_map = {}
|
236 |
+
for att in attachments:
|
237 |
+
# Обычно att["filename"] = "8a57f2de.jpg"
|
238 |
+
# а в doc-е: "image": "8a57f2de-df06-4a20-93af-a6e721fedfb2"
|
239 |
+
# Нужно сопоставить, часто это совпадает с `att["filename"]` без расширения,
|
240 |
+
# но бывает точное совпадение. Подгоняем логику по�� вашу структуру.
|
241 |
+
|
242 |
+
# Если "images":[{"image":"17b9f599-efc3-4bee-8d15-9ad24da9dfac"}]
|
243 |
+
# тогда ищем attachment, у которого filename = "17b9f599-efc3-4bee-8d15-9ad24da9dfac.jpg"
|
244 |
+
base_name = os.path.splitext(att["filename"])[0] # "17b9f599-efc3-4bee-8d15-9ad24da9dfac"
|
245 |
+
name_map[base_name] = att["filename"]
|
246 |
+
|
247 |
+
for node in content_arr:
|
248 |
+
ntype = node.get("type")
|
249 |
+
|
250 |
+
# 1) Абзац
|
251 |
+
if ntype == "paragraph":
|
252 |
+
paragraph_text = ""
|
253 |
+
if "content" in node:
|
254 |
+
for inline in node["content"]:
|
255 |
+
if inline.get("type") == "text":
|
256 |
+
paragraph_text += inline.get("text", "")
|
257 |
+
if paragraph_text.strip():
|
258 |
+
html_parts.append(f"<p>{paragraph_text}</p>")
|
259 |
+
|
260 |
+
# 2) Изображение
|
261 |
+
elif ntype == "image":
|
262 |
+
# атрибуты лежат в node["attrs"]["images"]
|
263 |
+
# это массив вида [{"image":"8a57f2de-df06-4a20-93af-a6e721fedfb2"}]
|
264 |
+
images_list = node.get("attrs", {}).get("images", [])
|
265 |
+
for img_obj in images_list:
|
266 |
+
img_name = img_obj.get("image") # "8a57f2de-df06-4a20-93af-a6e721fedfb2"
|
267 |
+
if not img_name:
|
268 |
+
continue
|
269 |
+
# Сопоставляем с attachments
|
270 |
+
filename = name_map.get(img_name)
|
271 |
+
if filename:
|
272 |
+
html_parts.append(f'<img src="imgs/{filename}"/>')
|
273 |
+
else:
|
274 |
+
# Если нет в attachments, пропустим
|
275 |
+
logging.warning(f"Не нашли attachment для {img_name}")
|
276 |
+
|
277 |
+
# 3) Любой другой тип (table, heading, list и пр.) - можно дописать по надобности
|
278 |
+
else:
|
279 |
+
# пока просто игнорируем или можно сделать ещё один <p>?
|
280 |
+
pass
|
281 |
+
|
282 |
+
return "\n".join(html_parts)
|
283 |
+
|
284 |
+
def _compress_image(self, img_path):
|
285 |
+
"""
|
286 |
+
Сжимаем (конвертируем) в JPEG, используем кэш, чтобы не обрабатывать повторно.
|
287 |
+
"""
|
288 |
+
if img_path in self._image_cache:
|
289 |
+
return self._image_cache[img_path]
|
290 |
+
|
291 |
+
try:
|
292 |
+
with Image.open(img_path) as im:
|
293 |
+
if im.mode != "RGB":
|
294 |
+
im = im.convert("RGB")
|
295 |
+
buf = io.BytesIO()
|
296 |
+
im.save(buf, format="JPEG", optimize=True, quality=self.image_quality)
|
297 |
+
buf.seek(0)
|
298 |
+
data = buf.read()
|
299 |
+
self._image_cache[img_path] = data
|
300 |
+
return data
|
301 |
+
except Exception as e:
|
302 |
+
logging.warning(f"Ошибка сжатия {img_path}: {e}")
|
303 |
+
return img_path.read_bytes()
|
304 |
+
|
305 |
+
def _make_title_page(self, style_item):
|
306 |
+
title = self.ranobe_data.get("title", "Без названия")
|
307 |
+
orig = self.ranobe_data.get("original_title", "")
|
308 |
+
desc = self.ranobe_data.get("description", "")
|
309 |
+
|
310 |
+
# Ссылка "Далее" -> первый том
|
311 |
+
volumes = [ch["volume"] for ch in self.ranobe_data["chapters"]]
|
312 |
+
link = "#"
|
313 |
+
if volumes:
|
314 |
+
try:
|
315 |
+
first_vol = sorted(volumes, key=lambda x: float(x))[0]
|
316 |
+
link = f"volume_{first_vol}.xhtml#volume_{first_vol}"
|
317 |
+
except:
|
318 |
+
pass
|
319 |
+
|
320 |
+
html = f"""
|
321 |
+
<h1 style="text-align:center;">{title}</h1>
|
322 |
+
<h2 style="text-align:center;">{orig}</h2>
|
323 |
+
<h3>Описание</h3>
|
324 |
+
<p>{desc}</p>
|
325 |
+
<p style="text-align:center;">
|
326 |
+
<a href="{link}" style="font-size:1.2em;">Далее »</a>
|
327 |
+
</p>
|
328 |
+
<h3>Содержание</h3>
|
329 |
+
<p>Используйте оглавление или кнопку «Далее».</p>
|
330 |
+
"""
|
331 |
+
page = epub.EpubHtml(
|
332 |
+
title="Титульная страница",
|
333 |
+
file_name="title_page.xhtml",
|
334 |
+
content=html
|
335 |
+
)
|
336 |
+
page.add_item(style_item)
|
337 |
+
return page
|
338 |
+
|
339 |
+
def _create_style(self):
|
340 |
+
css = '''
|
341 |
+
@namespace epub "http://www.idpf.org/2007/ops";
|
342 |
+
body {
|
343 |
+
font-family: Arial, sans-serif;
|
344 |
+
line-height: 1.6;
|
345 |
+
margin: 0 auto;
|
346 |
+
max-width: 800px;
|
347 |
+
}
|
348 |
+
h1, h2, h3 {
|
349 |
+
text-align: center;
|
350 |
+
margin: 1em 0;
|
351 |
+
}
|
352 |
+
p {
|
353 |
+
margin: 0.5em 0;
|
354 |
+
text-indent: 1.5em;
|
355 |
+
}
|
356 |
+
img {
|
357 |
+
display: block;
|
358 |
+
margin: 1em auto;
|
359 |
+
max-width: 100%;
|
360 |
+
}
|
361 |
+
'''
|
362 |
+
style_item = epub.EpubItem(
|
363 |
+
uid="main_style",
|
364 |
+
file_name="style/main.css",
|
365 |
+
media_type="text/css",
|
366 |
+
content=css
|
367 |
+
)
|
368 |
+
return style_item
|
369 |
+
|
370 |
+
def main():
|
371 |
+
print("Введите путь к ranobe.json:")
|
372 |
+
path = input().strip()
|
373 |
+
if not os.path.exists(path):
|
374 |
+
print("Файл не найден!")
|
375 |
+
return
|
376 |
+
try:
|
377 |
+
creator = EpubCreator(path, image_quality=85)
|
378 |
+
epub_file = creator.create_epub()
|
379 |
+
print(f"Готово! EPUB: {epub_file}")
|
380 |
+
except Exception as e:
|
381 |
+
logging.error(f"Ошибка: {e}")
|
382 |
+
print(f"Ошибка: {e}")
|
383 |
+
|
384 |
+
if __name__ == "__main__":
|
385 |
+
main()
|
get_ranobe_content.py
ADDED
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import os
|
3 |
+
import json
|
4 |
+
import time
|
5 |
+
from pathlib import Path
|
6 |
+
import requests
|
7 |
+
from tqdm import tqdm
|
8 |
+
from bs4 import BeautifulSoup # чтобы заменить ссылки прямо в HTML
|
9 |
+
|
10 |
+
logging.basicConfig(
|
11 |
+
level=logging.INFO,
|
12 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
13 |
+
handlers=[logging.StreamHandler()]
|
14 |
+
)
|
15 |
+
|
16 |
+
def extract_book_id(url):
|
17 |
+
"""
|
18 |
+
Извлекаем ID книги из URL (например /ru/book/1234--kniga, /ru/1234--kniga).
|
19 |
+
Возвращаем '1234--kniga' или None, если не получилось.
|
20 |
+
"""
|
21 |
+
import re
|
22 |
+
patterns = [
|
23 |
+
r'/ru/book/(\d+--[\w-]+)',
|
24 |
+
r'/ru/(\d+--[\w-]+)/',
|
25 |
+
]
|
26 |
+
for pat in patterns:
|
27 |
+
m = re.search(pat, url)
|
28 |
+
if m:
|
29 |
+
return m.group(1)
|
30 |
+
return None
|
31 |
+
|
32 |
+
def get_book_info(book_id):
|
33 |
+
"""
|
34 |
+
Получаем инфо о книге (название, описание).
|
35 |
+
"""
|
36 |
+
api_url = f"https://api2.mangalib.me/api/manga/{book_id}?fields[]=summary"
|
37 |
+
r = requests.get(api_url)
|
38 |
+
if r.status_code == 200:
|
39 |
+
return r.json().get('data', {})
|
40 |
+
return None
|
41 |
+
|
42 |
+
def get_cover_url(book_id):
|
43 |
+
"""
|
44 |
+
Получаем URL обложки.
|
45 |
+
"""
|
46 |
+
api_url = f"https://api2.mangalib.me/api/manga/{book_id}"
|
47 |
+
r = requests.get(api_url)
|
48 |
+
if r.status_code == 200:
|
49 |
+
data = r.json().get('data', {})
|
50 |
+
cover_data = data.get('cover', {})
|
51 |
+
return cover_data.get('default')
|
52 |
+
return None
|
53 |
+
|
54 |
+
def get_chapters_list(book_id):
|
55 |
+
"""
|
56 |
+
Получаем список глав: [ {"tom": int, "chapter": float, "name": str, "id": int}, ... ]
|
57 |
+
"""
|
58 |
+
api_url = f"https://api2.mangalib.me/api/manga/{book_id}/chapters"
|
59 |
+
r = requests.get(api_url)
|
60 |
+
if r.status_code == 200:
|
61 |
+
data = r.json().get('data', [])
|
62 |
+
chapters = []
|
63 |
+
for ch in data:
|
64 |
+
chapters.append({
|
65 |
+
"tom": int(ch['volume']),
|
66 |
+
"chapter": float(ch['number']),
|
67 |
+
"name": ch['name'],
|
68 |
+
"id": ch['id']
|
69 |
+
})
|
70 |
+
chapters.sort(key=lambda x: (x['tom'], x['chapter']))
|
71 |
+
return chapters
|
72 |
+
return []
|
73 |
+
|
74 |
+
def get_chapter_data(book_id, volume, chapter, max_retries=5, sleep_time=1):
|
75 |
+
"""
|
76 |
+
Получаем контент и вложения главы. Возвращаем словарь или None.
|
77 |
+
Повторяем до max_retries раз с паузой в sleep_time секунд,
|
78 |
+
если сервер не вернул код 200.
|
79 |
+
"""
|
80 |
+
if chapter.endswith('.0'):
|
81 |
+
chapter = chapter.split('.')[0]
|
82 |
+
api_url = f"https://api2.mangalib.me/api/manga/{book_id}/chapter?number={chapter}&volume={volume}"
|
83 |
+
|
84 |
+
for attempt in range(1, max_retries + 1):
|
85 |
+
try:
|
86 |
+
r = requests.get(api_url)
|
87 |
+
if r.status_code == 200:
|
88 |
+
return r.json().get('data')
|
89 |
+
else:
|
90 |
+
logging.warning(
|
91 |
+
f"Не удалось загрузить главу (статус {r.status_code}), попытка {attempt}/{max_retries}"
|
92 |
+
)
|
93 |
+
except Exception as e:
|
94 |
+
logging.error(f"Ошибка при запросе главы: {e}")
|
95 |
+
|
96 |
+
if attempt < max_retries:
|
97 |
+
time.sleep(sleep_time)
|
98 |
+
# Если все попытки провалились, возвращаем None
|
99 |
+
return None
|
100 |
+
|
101 |
+
def download_image(url, save_path, max_retries=5, sleep_time=1):
|
102 |
+
"""
|
103 |
+
Скачиваем картинку, сохраняем в save_path.
|
104 |
+
Повторяем до max_retries раз с паузой в sleep_time секунд,
|
105 |
+
если сервер не вернул код 200 или возникла ошибка.
|
106 |
+
"""
|
107 |
+
for attempt in range(1, max_retries + 1):
|
108 |
+
try:
|
109 |
+
if url.startswith("https://"):
|
110 |
+
resp = requests.get(url)
|
111 |
+
else:
|
112 |
+
# если url типа "/uploads/...":
|
113 |
+
resp = requests.get(f"https://ranobelib.me{url}")
|
114 |
+
|
115 |
+
if resp.status_code == 200:
|
116 |
+
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
117 |
+
with open(save_path, "wb") as f:
|
118 |
+
f.write(resp.content)
|
119 |
+
return True
|
120 |
+
else:
|
121 |
+
logging.warning(
|
122 |
+
f"Не удалось скачать {url}, код {resp.status_code}, попытка {attempt}/{max_retries}"
|
123 |
+
)
|
124 |
+
except Exception as e:
|
125 |
+
logging.error(f"Ошибка скачивания {url}: {e}")
|
126 |
+
|
127 |
+
if attempt < max_retries:
|
128 |
+
time.sleep(sleep_time)
|
129 |
+
return False
|
130 |
+
|
131 |
+
def fix_img_links_in_html(html_str, output_folder):
|
132 |
+
"""
|
133 |
+
На вход: исходный HTML (как строка), где могут быть <img loading="lazy" src="https://ranobelib.me/...">
|
134 |
+
Задача:
|
135 |
+
- ��айти все <img src="..."> (используем BeautifulSoup).
|
136 |
+
- Для каждого img, скачать локально (imgs/filename.jpg).
|
137 |
+
- Заменить src="..." на "imgs/filename.jpg".
|
138 |
+
Возвращаем новый HTML со всеми локальными ссылками.
|
139 |
+
"""
|
140 |
+
soup = BeautifulSoup(html_str, "html.parser")
|
141 |
+
imgs = soup.find_all("img")
|
142 |
+
for tag in imgs:
|
143 |
+
# Удаляем loading="lazy", если не нужно
|
144 |
+
if 'loading' in tag.attrs:
|
145 |
+
del tag.attrs['loading']
|
146 |
+
|
147 |
+
src_val = tag.get("src")
|
148 |
+
if not src_val:
|
149 |
+
continue
|
150 |
+
|
151 |
+
if src_val.startswith("http") or src_val.startswith("/uploads/"):
|
152 |
+
# Извлекаем имя файла
|
153 |
+
from urllib.parse import urlparse, unquote
|
154 |
+
parsed = urlparse(src_val)
|
155 |
+
filename = os.path.basename(parsed.path) # извлечём имя файла
|
156 |
+
if not filename:
|
157 |
+
filename = "img_unknown.jpg"
|
158 |
+
|
159 |
+
local_path = Path(output_folder) / "imgs" / filename
|
160 |
+
if download_image(src_val, local_path):
|
161 |
+
tag["src"] = f"imgs/{filename}"
|
162 |
+
# Иначе, если уже локальная, не трогаем.
|
163 |
+
return str(soup)
|
164 |
+
|
165 |
+
def fix_img_links_in_doc(doc_data, output_folder, attachments):
|
166 |
+
"""
|
167 |
+
Обработка контента в doc-формате (ProseMirror).
|
168 |
+
Зависит от структуры doc_data и attachments.
|
169 |
+
Здесь пример, где мы скачиваем файлы из attachments,
|
170 |
+
но не меняем напрямую сам doc (если ссылки на изображения
|
171 |
+
формируются автоматикой по имени).
|
172 |
+
"""
|
173 |
+
for att in attachments:
|
174 |
+
url = att['url']
|
175 |
+
filename = att['filename']
|
176 |
+
local_path = Path(output_folder) / "imgs" / filename
|
177 |
+
download_image(url, local_path)
|
178 |
+
return doc_data
|
179 |
+
|
180 |
+
def get_ranobe_content(book_url, output_dir="output",progress=None):
|
181 |
+
"""
|
182 |
+
Основная функция:
|
183 |
+
1) Извлекаем book_id
|
184 |
+
2) Скачиваем инфо о книге, обложку
|
185 |
+
3) Получаем список глав
|
186 |
+
4) Для каждой главы: получаем контент, скачиваем картинки (прямо в тексте <img>),
|
187 |
+
attachments (тоже скачиваем), подменяем src="..." на локальное (imgs/...)
|
188 |
+
5) Сохраняем ranobe.json (все ссылки уже локальные).
|
189 |
+
"""
|
190 |
+
# if progress:
|
191 |
+
progress(0, desc="Подготовка директорий")
|
192 |
+
|
193 |
+
out_path = Path(output_dir)
|
194 |
+
out_path.mkdir(parents=True, exist_ok=True)
|
195 |
+
imgs_path = out_path / "imgs"
|
196 |
+
imgs_path.mkdir(exist_ok=True)
|
197 |
+
|
198 |
+
# if progress:
|
199 |
+
progress(0.05, desc="Получение информации о книге")
|
200 |
+
|
201 |
+
|
202 |
+
book_id = extract_book_id(book_url)
|
203 |
+
if not book_id:
|
204 |
+
raise ValueError("Не удалось извлечь ID книги")
|
205 |
+
|
206 |
+
info = get_book_info(book_id)
|
207 |
+
if not info:
|
208 |
+
raise ValueError("Не удалось получить инфо о книге")
|
209 |
+
|
210 |
+
# if progress:
|
211 |
+
progress(0.1, desc="Загрузка обложки")
|
212 |
+
|
213 |
+
|
214 |
+
# Скачиваем обложку
|
215 |
+
cover_local = None
|
216 |
+
cover_url = get_cover_url(book_id)
|
217 |
+
if cover_url:
|
218 |
+
cover_filename = "cover" + Path(cover_url).suffix
|
219 |
+
cover_full_path = imgs_path / cover_filename
|
220 |
+
if download_image(cover_url, cover_full_path):
|
221 |
+
cover_local = f"imgs/{cover_filename}"
|
222 |
+
|
223 |
+
# if progress:
|
224 |
+
progress(0.15, desc="Получение списка глав")
|
225 |
+
|
226 |
+
|
227 |
+
# Получаем список глав
|
228 |
+
chapters_list = get_chapters_list(book_id)
|
229 |
+
logging.info(f"Найдено глав: {len(chapters_list)}")
|
230 |
+
|
231 |
+
all_chapters = []
|
232 |
+
|
233 |
+
# Используем progress.tqdm для отслеживания прогресса
|
234 |
+
if True:
|
235 |
+
chapters_iter = progress.tqdm(chapters_list, desc="Загрузка глав")
|
236 |
+
# else:
|
237 |
+
# chapters_iter = tqdm(chapters_list, desc="Загрузка глав")
|
238 |
+
|
239 |
+
for ch in chapters_iter:
|
240 |
+
tom = str(ch['tom'])
|
241 |
+
chap_str = str(ch['chapter'])
|
242 |
+
ch_data = get_chapter_data(book_id, tom, chap_str)
|
243 |
+
if not ch_data:
|
244 |
+
logging.warning(f"Пропускаем главу {tom} {chap_str} (не удалось загрузить).")
|
245 |
+
continue
|
246 |
+
|
247 |
+
attachments = ch_data.get("attachments", [])
|
248 |
+
content = ch_data.get("content", "")
|
249 |
+
|
250 |
+
# Если контент строковый (HTML)
|
251 |
+
if isinstance(content, str):
|
252 |
+
new_html = fix_img_links_in_html(content, out_path)
|
253 |
+
content = new_html
|
254 |
+
# Если контент doc-формат
|
255 |
+
elif isinstance(content, dict) and content.get("type") == "doc":
|
256 |
+
content = fix_img_links_in_doc(content, out_path, attachments)
|
257 |
+
|
258 |
+
# Скачиваем все attachments (часто совпадают с изображениями в тексте)
|
259 |
+
for att in attachments:
|
260 |
+
url = att["url"]
|
261 |
+
fname = att["filename"]
|
262 |
+
local_file = imgs_path / fname
|
263 |
+
download_image(url, local_file)
|
264 |
+
|
265 |
+
# Формируем запись о главе
|
266 |
+
chapter_rec = {
|
267 |
+
"id": ch_data["id"],
|
268 |
+
"volume": ch_data["volume"],
|
269 |
+
"chapter": ch_data["number"],
|
270 |
+
"name": ch_data["name"],
|
271 |
+
"attachments": attachments,
|
272 |
+
"content": content
|
273 |
+
}
|
274 |
+
all_chapters.append(chapter_rec)
|
275 |
+
|
276 |
+
# if progress:
|
277 |
+
progress(0.95, desc="Сохранение результатов")
|
278 |
+
|
279 |
+
|
280 |
+
# Формируем итоговую структуру и сохраняем
|
281 |
+
ranobe_data = {
|
282 |
+
"id": book_id,
|
283 |
+
"title": info.get("rus_name", "Без названия"),
|
284 |
+
"original_title": info.get("name", ""),
|
285 |
+
"description": info.get("summary", ""),
|
286 |
+
"cover_image": cover_local, # "imgs/cover.jpg" или None
|
287 |
+
"chapters": all_chapters
|
288 |
+
}
|
289 |
+
|
290 |
+
ranobe_json_path = out_path / "ranobe.json"
|
291 |
+
with open(ranobe_json_path, "w", encoding="utf-8") as f:
|
292 |
+
json.dump(ranobe_data, f, ensure_ascii=False, indent=2)
|
293 |
+
logging.info(f"Сохранён ranobe.json: {ranobe_json_path}")
|
294 |
+
|
295 |
+
# if progress:
|
296 |
+
progress(1.0, desc="Готово")
|
297 |
+
|
298 |
+
return str(ranobe_json_path)
|
299 |
+
|
300 |
+
def main():
|
301 |
+
url = input("Введите URL книги: ").strip()
|
302 |
+
try:
|
303 |
+
rjson = get_ranobe_content(url, output_dir="output")
|
304 |
+
print(f"Готово! Данные о книге в: {rjson}")
|
305 |
+
except Exception as e:
|
306 |
+
logging.error(f"Ошибка: {e}")
|
307 |
+
print(f"Ошибка: {e}")
|
308 |
+
|
309 |
+
if __name__ == "__main__":
|
310 |
+
main()
|
pipeline.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from get_ranobe_content import get_ranobe_content
|
3 |
+
from create_epub import EpubCreator
|
4 |
+
import gradio as gr
|
5 |
+
logging.basicConfig(
|
6 |
+
level=logging.INFO,
|
7 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
8 |
+
handlers=[
|
9 |
+
logging.StreamHandler()
|
10 |
+
]
|
11 |
+
)
|
12 |
+
|
13 |
+
def run_pipeline(book_url, output_dir="output", progress=None):
|
14 |
+
"""
|
15 |
+
Запускает полный цикл:
|
16 |
+
|
17 |
+
1) Получение контента (get_ranobe_content)
|
18 |
+
2) Создание EPUB (create_epub)
|
19 |
+
Возвращает путь к созданному EPUB-файлу.
|
20 |
+
"""
|
21 |
+
|
22 |
+
progress(0, desc="Начинаем обработку")
|
23 |
+
|
24 |
+
|
25 |
+
# 1. Скачиваем данные ранобэ и картинки
|
26 |
+
progress(0.1, desc="Получение контента ранобэ")
|
27 |
+
ranobe_json_path = get_ranobe_content(book_url, output_dir=output_dir, progress=progress)
|
28 |
+
|
29 |
+
progress(0.8, desc="Создание EPUB файла")
|
30 |
+
creator = EpubCreator(ranobe_json_path, image_quality=85)
|
31 |
+
epub_path = creator.create_epub()
|
32 |
+
|
33 |
+
progress(1.0, desc="Готово")
|
34 |
+
return epub_path
|
35 |
+
|
36 |
+
def main():
|
37 |
+
"""
|
38 |
+
Примерный вызов для полного конвейера:
|
39 |
+
python pipeline.py
|
40 |
+
"""
|
41 |
+
|
42 |
+
url = input("Введите URL ранобэ (любой URL с сайта с ID книги): ").strip()
|
43 |
+
try:
|
44 |
+
epub_path = run_pipeline(url, output_dir="output")
|
45 |
+
print(f"Готово! EPUB создан в: {epub_path}")
|
46 |
+
except Exception as e:
|
47 |
+
print(f"Ошибка: {e}")
|
48 |
+
|
49 |
+
if __name__ == "__main__":
|
50 |
+
main()
|
requirements.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
ebooklib
|
2 |
+
gradio
|
3 |
+
beautifulsoup4
|