daswer123 commited on
Commit
7ea5b04
·
verified ·
1 Parent(s): 447f86c

Upload 6 files

Browse files
Files changed (6) hide show
  1. README.md +65 -14
  2. app.py +76 -0
  3. create_epub.py +385 -0
  4. get_ranobe_content.py +310 -0
  5. pipeline.py +50 -0
  6. requirements.txt +3 -0
README.md CHANGED
@@ -1,14 +1,65 @@
1
- ---
2
- title: Ranobelib Me To Epub
3
- emoji: 🐨
4
- colorFrom: red
5
- colorTo: gray
6
- sdk: gradio
7
- sdk_version: 5.15.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- short_description: 'Конвертация любых ранобе с сайта Ranobelib.me в epub формат '
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;">Далее &raquo;</a>
327
+ </p>
328
+ <h3>Содержание</h3>
329
+ <p>Используйте оглавление или кнопку &laquo;Далее&raquo;.</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