epub-browser 0.1.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.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: epub-browser
3
+ Version: 0.1.0
4
+ Summary: A tool to open epub files and serve them via a local web server for reading in a browser.
5
+ Home-page: https://github.com/dfface/epub-browser
6
+ Author: dfface
7
+ Author-email: dfface@sina.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: requires-python
20
+ Dynamic: summary
21
+
22
+ # epub-browser
23
+
24
+ Read epub file in the browser(Chrome/Edge/Safari...).
25
+
26
+ ## Usage
27
+
28
+ Type the command in the terminal:
29
+
30
+ ```bash
31
+ pip install epub-browser
32
+ epub-browser path/to/xxx.epub
33
+ ```
34
+
35
+ Then a browser will be opened to view the epub file.
36
+
37
+ ![epub on web](assets/test1.png)
38
+
39
+ ## Tips
40
+
41
+ You can combine web reading with the web extension called [Circle Reader](https://circlereader.com/) to gain more elegant experience.
42
+
43
+ Other extensions that are recommended are:
44
+
45
+ 1. [Diigo](https://www.diigo.com/): Read more effectively with annotation tools.
46
+ 2. ...
@@ -0,0 +1,25 @@
1
+ # epub-browser
2
+
3
+ Read epub file in the browser(Chrome/Edge/Safari...).
4
+
5
+ ## Usage
6
+
7
+ Type the command in the terminal:
8
+
9
+ ```bash
10
+ pip install epub-browser
11
+ epub-browser path/to/xxx.epub
12
+ ```
13
+
14
+ Then a browser will be opened to view the epub file.
15
+
16
+ ![epub on web](assets/test1.png)
17
+
18
+ ## Tips
19
+
20
+ You can combine web reading with the web extension called [Circle Reader](https://circlereader.com/) to gain more elegant experience.
21
+
22
+ Other extensions that are recommended are:
23
+
24
+ 1. [Diigo](https://www.diigo.com/): Read more effectively with annotation tools.
25
+ 2. ...
File without changes
@@ -0,0 +1,690 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ EPUB to Web Converter
4
+ 将EPUB文件转换为可在浏览器中阅读的网页格式
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import argparse
10
+ import zipfile
11
+ import tempfile
12
+ import shutil
13
+ import webbrowser
14
+ from http.server import HTTPServer, SimpleHTTPRequestHandler
15
+ from urllib.parse import unquote
16
+ import xml.etree.ElementTree as ET
17
+ import json
18
+ import re
19
+
20
+ class EPUBProcessor:
21
+ """处理EPUB文件的类"""
22
+
23
+ def __init__(self, epub_path):
24
+ self.epub_path = epub_path
25
+ self.temp_dir = tempfile.mkdtemp(prefix='epub_')
26
+ self.extract_dir = os.path.join(self.temp_dir, 'extracted')
27
+ self.web_dir = os.path.join(self.temp_dir, 'web')
28
+ self.book_title = "EPUB Book"
29
+ self.chapters = []
30
+ self.toc = [] # 存储目录结构
31
+ self.resources_base = "resources" # 资源文件的基础路径
32
+
33
+ def extract_epub(self):
34
+ """解压EPUB文件"""
35
+ try:
36
+ with zipfile.ZipFile(self.epub_path, 'r') as zip_ref:
37
+ zip_ref.extractall(self.extract_dir)
38
+ print(f"已解压EPUB文件到: {self.extract_dir}")
39
+ return True
40
+ except Exception as e:
41
+ print(f"解压EPUB文件失败: {e}")
42
+ return False
43
+
44
+ def parse_container(self):
45
+ """解析container.xml获取内容文件路径"""
46
+ container_path = os.path.join(self.extract_dir, 'META-INF', 'container.xml')
47
+ if not os.path.exists(container_path):
48
+ print("未找到container.xml文件")
49
+ return None
50
+
51
+ try:
52
+ tree = ET.parse(container_path)
53
+ root = tree.getroot()
54
+ # 查找rootfile元素
55
+ ns = {'ns': 'urn:oasis:names:tc:opendocument:xmlns:container'}
56
+ rootfile = root.find('.//ns:rootfile', ns)
57
+ if rootfile is not None:
58
+ return rootfile.get('full-path')
59
+ except Exception as e:
60
+ print(f"解析container.xml失败: {e}")
61
+
62
+ return None
63
+
64
+ def find_ncx_file(self, opf_path, manifest):
65
+ """查找NCX文件路径"""
66
+ opf_dir = os.path.dirname(opf_path)
67
+
68
+ # 首先查找OPF中明确指定的toc
69
+ try:
70
+ tree = ET.parse(os.path.join(self.extract_dir, opf_path))
71
+ root = tree.getroot()
72
+ ns = {'opf': 'http://www.idpf.org/2007/opf'}
73
+
74
+ spine = root.find('.//opf:spine', ns)
75
+ if spine is not None:
76
+ toc_id = spine.get('toc')
77
+ if toc_id and toc_id in manifest:
78
+ ncx_path = os.path.join(opf_dir, manifest[toc_id]['href'])
79
+ if os.path.exists(os.path.join(self.extract_dir, ncx_path)):
80
+ return ncx_path
81
+ except Exception as e:
82
+ print(f"查找toc属性失败: {e}")
83
+
84
+ # 如果没有明确指定,查找media-type为application/x-dtbncx+xml的文件
85
+ for item_id, item in manifest.items():
86
+ if item['media_type'] == 'application/x-dtbncx+xml':
87
+ ncx_path = os.path.join(opf_dir, item['href'])
88
+ if os.path.exists(os.path.join(self.extract_dir, ncx_path)):
89
+ return ncx_path
90
+
91
+ # 最后,尝试查找常见的NCX文件名
92
+ common_ncx_names = ['toc.ncx', 'nav.ncx', 'ncx.ncx']
93
+ for name in common_ncx_names:
94
+ ncx_path = os.path.join(opf_dir, name)
95
+ if os.path.exists(os.path.join(self.extract_dir, ncx_path)):
96
+ return ncx_path
97
+
98
+ return None
99
+
100
+ def parse_ncx(self, ncx_path):
101
+ """解析NCX文件获取目录结构"""
102
+ ncx_full_path = os.path.join(self.extract_dir, ncx_path)
103
+ if not os.path.exists(ncx_full_path):
104
+ print(f"未找到NCX文件: {ncx_full_path}")
105
+ return []
106
+
107
+ try:
108
+ # 读取文件内容并注册命名空间
109
+ with open(ncx_full_path, 'r', encoding='utf-8') as f:
110
+ content = f.read()
111
+
112
+ # 注册命名空间
113
+ ET.register_namespace('', 'http://www.daisy.org/z3986/2005/ncx/')
114
+
115
+ tree = ET.parse(ncx_full_path)
116
+ root = tree.getroot()
117
+
118
+ # 获取书籍标题
119
+ doc_title = root.find('.//{http://www.daisy.org/z3986/2005/ncx/}docTitle/{http://www.daisy.org/z3986/2005/ncx/}text')
120
+ if doc_title is not None and doc_title.text:
121
+ self.book_title = doc_title.text
122
+
123
+ # 解析目录
124
+ nav_map = root.find('.//{http://www.daisy.org/z3986/2005/ncx/}navMap')
125
+ if nav_map is None:
126
+ return []
127
+
128
+ toc = []
129
+
130
+ # 递归处理navPoint
131
+ def process_navpoint(navpoint, level=0):
132
+ # 获取导航标签和内容源
133
+ nav_label = navpoint.find('.//{http://www.daisy.org/z3986/2005/ncx/}navLabel/{http://www.daisy.org/z3986/2005/ncx/}text')
134
+ content = navpoint.find('.//{http://www.daisy.org/z3986/2005/ncx/}content')
135
+
136
+ if nav_label is not None and content is not None:
137
+ title = nav_label.text
138
+ src = content.get('src')
139
+
140
+ # 处理可能的锚点
141
+ if '#' in src:
142
+ src = src.split('#')[0]
143
+
144
+ if title and src:
145
+ # 将src路径转换为相对于EPUB根目录的完整路径
146
+ ncx_dir = os.path.dirname(ncx_path)
147
+ full_src = os.path.normpath(os.path.join(ncx_dir, src))
148
+
149
+ toc.append({
150
+ 'title': title,
151
+ 'src': full_src,
152
+ 'level': level
153
+ })
154
+
155
+ # 处理子navPoint
156
+ child_navpoints = navpoint.findall('{http://www.daisy.org/z3986/2005/ncx/}navPoint')
157
+ for child in child_navpoints:
158
+ process_navpoint(child, level + 1)
159
+
160
+ # 处理所有顶级navPoint
161
+ top_navpoints = nav_map.findall('{http://www.daisy.org/z3986/2005/ncx/}navPoint')
162
+ for navpoint in top_navpoints:
163
+ process_navpoint(navpoint, 0)
164
+
165
+ print(f"解析NCX得到目录项: {[(t['title'], t['src']) for t in toc]}")
166
+ return toc
167
+
168
+ except Exception as e:
169
+ print(f"解析NCX文件失败: {e}")
170
+ import traceback
171
+ traceback.print_exc()
172
+ return []
173
+
174
+ def parse_opf(self, opf_path):
175
+ """解析OPF文件获取书籍信息和章节列表"""
176
+ opf_full_path = os.path.join(self.extract_dir, opf_path)
177
+ if not os.path.exists(opf_full_path):
178
+ print(f"未找到OPF文件: {opf_full_path}")
179
+ return False
180
+
181
+ try:
182
+ tree = ET.parse(opf_full_path)
183
+ root = tree.getroot()
184
+
185
+ # 获取命名空间
186
+ ns = {'opf': 'http://www.idpf.org/2007/opf',
187
+ 'dc': 'http://purl.org/dc/elements/1.1/'}
188
+
189
+ # 获取书名
190
+ title_elem = root.find('.//dc:title', ns)
191
+ if title_elem is not None and title_elem.text:
192
+ self.book_title = title_elem.text
193
+
194
+ # 获取manifest(所有资源)
195
+ manifest = {}
196
+ opf_dir = os.path.dirname(opf_path)
197
+ for item in root.findall('.//opf:item', ns):
198
+ item_id = item.get('id')
199
+ href = item.get('href')
200
+ media_type = item.get('media-type', '')
201
+ # 构建相对于EPUB根目录的完整路径
202
+ full_path = os.path.normpath(os.path.join(opf_dir, href)) if href else None
203
+ manifest[item_id] = {
204
+ 'href': href,
205
+ 'media_type': media_type,
206
+ 'full_path': full_path
207
+ }
208
+
209
+ # 查找并解析NCX文件
210
+ ncx_path = self.find_ncx_file(opf_path, manifest)
211
+ if ncx_path:
212
+ self.toc = self.parse_ncx(ncx_path)
213
+ print(f"从NCX文件中找到 {len(self.toc)} 个目录项")
214
+
215
+ # 获取spine(阅读顺序)
216
+ spine = root.find('.//opf:spine', ns)
217
+ if spine is not None:
218
+ for itemref in spine.findall('opf:itemref', ns):
219
+ idref = itemref.get('idref')
220
+ if idref in manifest:
221
+ item = manifest[idref]
222
+ # 只处理HTML/XHTML内容
223
+ if item['media_type'] in ['application/xhtml+xml', 'text/html']:
224
+ # 尝试从toc中查找对应的标题
225
+ title = self.find_chapter_title(item['full_path'])
226
+
227
+ self.chapters.append({
228
+ 'id': idref,
229
+ 'path': item['full_path'],
230
+ 'title': title or f"Chapter {len(self.chapters) + 1}"
231
+ })
232
+
233
+ print(f"找到 {len(self.chapters)} 个章节")
234
+ print(f"章节列表: {[(c['title'], c['path']) for c in self.chapters]}")
235
+ return True
236
+
237
+ except Exception as e:
238
+ print(f"解析OPF文件失败: {e}")
239
+ return False
240
+
241
+ def find_chapter_title(self, chapter_path):
242
+ """根据章节路径在toc中查找对应的标题"""
243
+ # 先尝试精确匹配
244
+ for toc_item in self.toc:
245
+ if toc_item['src'] == chapter_path:
246
+ return toc_item['title']
247
+
248
+ # 如果直接匹配失败,尝试基于文件名匹配
249
+ chapter_filename = os.path.basename(chapter_path)
250
+ for toc_item in self.toc:
251
+ toc_filename = os.path.basename(toc_item['src'])
252
+ if toc_filename == chapter_filename:
253
+ return toc_item['title']
254
+
255
+ # 尝试规范化路径后再匹配
256
+ normalized_chapter_path = os.path.normpath(chapter_path)
257
+ for toc_item in self.toc:
258
+ normalized_toc_path = os.path.normpath(toc_item['src'])
259
+ if normalized_toc_path == normalized_chapter_path:
260
+ return toc_item['title']
261
+
262
+ print(f"未找到章节标题: {chapter_path}")
263
+ return None
264
+
265
+ def create_web_interface(self):
266
+ """创建网页界面"""
267
+ os.makedirs(self.web_dir, exist_ok=True)
268
+
269
+ # 创建主页面
270
+ self.create_index_page()
271
+
272
+ # 创建章节页面
273
+ self.create_chapter_pages()
274
+
275
+ # 复制资源文件(CSS、图片、字体等)
276
+ self.copy_resources()
277
+
278
+ print(f"网页界面已创建在: {self.web_dir}")
279
+
280
+ def create_index_page(self):
281
+ """创建索引页面"""
282
+ index_html = f"""<!DOCTYPE html>
283
+ <html lang="zh-CN">
284
+ <head>
285
+ <meta charset="UTF-8">
286
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
287
+ <title>{self.book_title}</title>
288
+ <style>
289
+ body {{
290
+ font-family: Arial, sans-serif;
291
+ max-width: 800px;
292
+ margin: 0 auto;
293
+ padding: 20px;
294
+ line-height: 1.6;
295
+ }}
296
+ .header {{
297
+ text-align: center;
298
+ margin-bottom: 40px;
299
+ border-bottom: 2px solid #333;
300
+ padding-bottom: 20px;
301
+ }}
302
+ .chapter-list {{
303
+ list-style-type: none;
304
+ padding: 0;
305
+ }}
306
+ .chapter-list li {{
307
+ margin: 5px 0;
308
+ padding: 8px 10px;
309
+ border-left: 3px solid #0066cc;
310
+ background-color: #f9f9f9;
311
+ }}
312
+ .chapter-list a {{
313
+ text-decoration: none;
314
+ color: #333;
315
+ display: block;
316
+ }}
317
+ .chapter-list a:hover {{
318
+ color: #0066cc;
319
+ background-color: #f0f0f0;
320
+ }}
321
+ .toc-level-0 {{ margin-left: 0px; }}
322
+ .toc-level-1 {{ margin-left: 20px; font-size: 0.95em; }}
323
+ .toc-level-2 {{ margin-left: 40px; font-size: 0.9em; }}
324
+ .toc-level-3 {{ margin-left: 60px; font-size: 0.85em; }}
325
+ </style>
326
+ </head>
327
+ <body>
328
+ <div class="header">
329
+ <h1>{self.book_title}</h1>
330
+ <p>EPUB to Web Converter</p>
331
+ </div>
332
+
333
+ <h2>目录</h2>
334
+ <ul class="chapter-list">
335
+ """
336
+
337
+ # 如果有详细的toc信息,使用toc生成目录
338
+ if self.toc:
339
+ # 创建章节路径到索引的映射
340
+ chapter_index_map = {}
341
+ for i, chapter in enumerate(self.chapters):
342
+ chapter_index_map[chapter['path']] = i
343
+
344
+ print(f"章节索引映射: {chapter_index_map}")
345
+
346
+ # 根据toc生成目录
347
+ for toc_item in self.toc:
348
+ level_class = f"toc-level-{min(toc_item.get('level', 0), 3)}"
349
+ chapter_index = chapter_index_map.get(toc_item['src'])
350
+
351
+ if chapter_index is not None:
352
+ index_html += f' <li class="{level_class}"><a href="chapter_{chapter_index}.html">{toc_item["title"]}</a></li>\n'
353
+ else:
354
+ print(f"未找到章节索引: {toc_item['src']}")
355
+ else:
356
+ # 回退到简单章节列表
357
+ for i, chapter in enumerate(self.chapters):
358
+ index_html += f' <li><a href="chapter_{i}.html">{chapter["title"]}</a></li>\n'
359
+
360
+ index_html += """ </ul>
361
+ </body>
362
+ </html>"""
363
+
364
+ with open(os.path.join(self.web_dir, 'index.html'), 'w', encoding='utf-8') as f:
365
+ f.write(index_html)
366
+
367
+ def create_chapter_pages(self):
368
+ """创建章节页面"""
369
+ for i, chapter in enumerate(self.chapters):
370
+ chapter_path = os.path.join(self.extract_dir, chapter['path'])
371
+
372
+ if os.path.exists(chapter_path):
373
+ try:
374
+ # 读取章节内容
375
+ with open(chapter_path, 'r', encoding='utf-8') as f:
376
+ content = f.read()
377
+
378
+ # 处理HTML内容,修复资源链接并提取样式
379
+ body_content, style_links = self.process_html_content(content, chapter['path'])
380
+
381
+ # 创建章节页面
382
+ chapter_html = self.create_chapter_template(body_content, style_links, i, chapter['title'])
383
+
384
+ with open(os.path.join(self.web_dir, f'chapter_{i}.html'), 'w', encoding='utf-8') as f:
385
+ f.write(chapter_html)
386
+
387
+ except Exception as e:
388
+ print(f"处理章节 {chapter['path']} 失败: {e}")
389
+
390
+ def process_html_content(self, content, chapter_path):
391
+ """处理HTML内容,修复资源链接并提取样式"""
392
+ # 提取head中的样式链接
393
+ style_links = self.extract_style_links(content, chapter_path)
394
+
395
+ # 提取body内容
396
+ body_content = self.clean_html_content(content)
397
+
398
+ # 修复body中的图片链接
399
+ body_content = self.fix_image_links(body_content, chapter_path)
400
+
401
+ # 修复body中的其他资源链接
402
+ body_content = self.fix_other_links(body_content, chapter_path)
403
+
404
+ return body_content, style_links
405
+
406
+ def extract_style_links(self, content, chapter_path):
407
+ """从head中提取样式链接"""
408
+ style_links = []
409
+
410
+ # 匹配head标签
411
+ head_match = re.search(r'<head[^>]*>(.*?)</head>', content, re.DOTALL | re.IGNORECASE)
412
+ if head_match:
413
+ head_content = head_match.group(1)
414
+
415
+ # 匹配link标签(CSS样式表)
416
+ link_pattern = r'<link[^>]+rel=["\']stylesheet["\'][^>]*>'
417
+ links = re.findall(link_pattern, head_content, re.IGNORECASE)
418
+
419
+ for link in links:
420
+ # 提取href属性
421
+ href_match = re.search(r'href=["\']([^"\']+)["\']', link)
422
+ if href_match:
423
+ href = href_match.group(1)
424
+ # 如果已经是绝对路径,则不处理
425
+ if href.startswith(('http://', 'https://', '/')):
426
+ style_links.append(link)
427
+ else:
428
+ # 计算相对于EPUB根目录的完整路径
429
+ chapter_dir = os.path.dirname(chapter_path)
430
+ full_href = os.path.normpath(os.path.join(chapter_dir, href))
431
+
432
+ # 转换为web资源路径
433
+ web_href = f"{self.resources_base}/{full_href}"
434
+
435
+ # 替换href属性
436
+ fixed_link = link.replace(f'href="{href}"', f'href="{web_href}"')
437
+ style_links.append(fixed_link)
438
+
439
+ # 匹配style标签
440
+ style_pattern = r'<style[^>]*>.*?</style>'
441
+ styles = re.findall(style_pattern, head_content, re.DOTALL)
442
+ style_links.extend(styles)
443
+
444
+ return '\n '.join(style_links)
445
+
446
+ def clean_html_content(self, content):
447
+ """清理HTML内容"""
448
+ # 提取body内容(如果存在)
449
+ if '<body' in content.lower():
450
+ try:
451
+ # 提取body内容
452
+ start = content.lower().find('<body')
453
+ start = content.find('>', start) + 1
454
+ end = content.lower().find('</body>')
455
+ content = content[start:end]
456
+ except:
457
+ pass
458
+
459
+ return content
460
+
461
+ def fix_image_links(self, content, chapter_path):
462
+ """修复图片链接"""
463
+ # 匹配img标签的src属性
464
+ img_pattern = r'<img[^>]+src="([^"]+)"[^>]*>'
465
+
466
+ def replace_img_link(match):
467
+ src = match.group(1)
468
+ # 如果已经是绝对路径或数据URI,则不处理
469
+ if src.startswith(('http://', 'https://', 'data:', '/')):
470
+ return match.group(0)
471
+
472
+ # 计算相对于EPUB根目录的完整路径
473
+ chapter_dir = os.path.dirname(chapter_path)
474
+ full_src = os.path.normpath(os.path.join(chapter_dir, src))
475
+
476
+ # 转换为web资源路径
477
+ web_src = f"{self.resources_base}/{full_src}"
478
+ return match.group(0).replace(f'src="{src}"', f'src="{web_src}"')
479
+
480
+ return re.sub(img_pattern, replace_img_link, content)
481
+
482
+ def fix_other_links(self, content, chapter_path):
483
+ """修复其他资源链接"""
484
+ # 匹配其他可能包含资源链接的属性
485
+ link_patterns = [
486
+ (r'url\(\s*[\'"]?([^\'"\)]+)[\'"]?\s*\)', 'url'), # CSS中的url()
487
+ ]
488
+
489
+ for pattern, attr_type in link_patterns:
490
+ def replace_other_link(match):
491
+ url = match.group(1)
492
+ # 如果已经是绝对路径或数据URI,则不处理
493
+ if url.startswith(('http://', 'https://', 'data:', '/')):
494
+ return match.group(0)
495
+
496
+ # 计算相对于EPUB根目录的完整路径
497
+ chapter_dir = os.path.dirname(chapter_path)
498
+ full_url = os.path.normpath(os.path.join(chapter_dir, url))
499
+
500
+ # 转换为web资源路径
501
+ web_url = f"{self.resources_base}/{full_url}"
502
+ return match.group(0).replace(url, web_url)
503
+
504
+ content = re.sub(pattern, replace_other_link, content)
505
+
506
+ return content
507
+
508
+ def create_chapter_template(self, content, style_links, chapter_index, chapter_title):
509
+ """创建章节页面模板"""
510
+ prev_link = f'<a href="chapter_{chapter_index-1}.html">上一章</a>' if chapter_index > 0 else ''
511
+ next_link = f'<a href="chapter_{chapter_index+1}.html">下一章</a>' if chapter_index < len(self.chapters) - 1 else ''
512
+
513
+ return f"""<!DOCTYPE html>
514
+ <html lang="zh-CN">
515
+ <head>
516
+ <meta charset="UTF-8">
517
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
518
+ <title>{chapter_title} - {self.book_title}</title>
519
+ {style_links}
520
+ <style>
521
+ body {{
522
+ max-width: 800px;
523
+ margin: 0 auto;
524
+ padding: 20px;
525
+ line-height: 1.6;
526
+ }}
527
+ .navigation {{
528
+ display: flex;
529
+ justify-content: space-between;
530
+ margin: 20px 0;
531
+ padding: 10px 0;
532
+ border-top: 1px solid #ddd;
533
+ border-bottom: 1px solid #ddd;
534
+ }}
535
+ .content {{
536
+ margin: 20px 0;
537
+ }}
538
+ .chapter-title {{
539
+ text-align: center;
540
+ margin-bottom: 30px;
541
+ padding-bottom: 10px;
542
+ border-bottom: 1px solid #eee;
543
+ }}
544
+ a {{
545
+ color: #0066cc;
546
+ text-decoration: none;
547
+ }}
548
+ a:hover {{
549
+ background-color: #0066cc;
550
+ color: white;
551
+ }}
552
+ img {{
553
+ max-width: 100%;
554
+ height: auto;
555
+ }}
556
+ </style>
557
+ <link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/default.min.css">
558
+ <!-- and it's easy to individually load additional languages -->
559
+ <script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/languages/go.min.js"></script>
560
+ <script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/languages/python.min.js"></script>
561
+ <script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/languages/bash.min.js"></script>
562
+ <script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/languages/java.min.js"></script>
563
+ <script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/languages/c++.min.js"></script>
564
+ <script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/languages/c.min.js"></script>
565
+ <script>hljs.highlightAll();</script>
566
+ <script>
567
+ document.addEventListener('DOMContentLoaded', (event) => {{
568
+ document.querySelectorAll('pre').forEach((el) => {{
569
+ hljs.highlightElement(el);
570
+ }});
571
+ }});
572
+ </script>
573
+ </head>
574
+ <body>
575
+ <div class="navigation">
576
+ <div>{prev_link}</div>
577
+ <div><a href="index.html">目录</a></div>
578
+ <div>{next_link}</div>
579
+ </div>
580
+
581
+ <article class="content">
582
+ {content}
583
+ </article>
584
+
585
+ <div class="navigation">
586
+ <div>{prev_link}</div>
587
+ <div><a href="index.html">目录</a></div>
588
+ <div>{next_link}</div>
589
+ </div>
590
+ </body>
591
+ </html>"""
592
+
593
+ def copy_resources(self):
594
+ """复制资源文件"""
595
+ # 复制整个提取目录到web目录下的resources文件夹
596
+ resources_dir = os.path.join(self.web_dir, self.resources_base)
597
+ os.makedirs(resources_dir, exist_ok=True)
598
+
599
+ # 复制整个提取目录
600
+ for root, dirs, files in os.walk(self.extract_dir):
601
+ for file in files:
602
+ src_path = os.path.join(root, file)
603
+ # 计算相对于提取目录的相对路径
604
+ rel_path = os.path.relpath(src_path, self.extract_dir)
605
+ dst_path = os.path.join(resources_dir, rel_path)
606
+
607
+ # 确保目标目录存在
608
+ os.makedirs(os.path.dirname(dst_path), exist_ok=True)
609
+ shutil.copy2(src_path, dst_path)
610
+
611
+ print(f"资源文件已复制到: {resources_dir}")
612
+
613
+ def cleanup(self):
614
+ """清理临时文件"""
615
+ if os.path.exists(self.temp_dir):
616
+ shutil.rmtree(self.temp_dir)
617
+ print("临时文件已清理")
618
+
619
+ class EPUBHTTPRequestHandler(SimpleHTTPRequestHandler):
620
+ """自定义HTTP请求处理器"""
621
+
622
+ def __init__(self, *args, web_dir=None, **kwargs):
623
+ self.web_dir = web_dir
624
+ super().__init__(*args, directory=web_dir, **kwargs)
625
+
626
+ def log_message(self, format, *args):
627
+ """自定义日志格式"""
628
+ print(f"[{self.log_date_time_string()}] {format % args}")
629
+
630
+ def main():
631
+ parser = argparse.ArgumentParser(description='EPUB to Web Converter')
632
+ parser.add_argument('filename', help='EPUB文件路径')
633
+ parser.add_argument('--port', '-p', type=int, default=8000, help='Web服务器端口 (默认: 8000)')
634
+ parser.add_argument('--no-browser', action='store_true', help='不自动打开浏览器')
635
+
636
+ args = parser.parse_args()
637
+
638
+ if not os.path.exists(args.filename):
639
+ print(f"错误: 文件 '{args.filename}' 不存在")
640
+ sys.exit(1)
641
+
642
+ # 处理EPUB文件
643
+ processor = EPUBProcessor(args.filename)
644
+
645
+ try:
646
+ print(f"正在处理EPUB文件: {args.filename}")
647
+
648
+ # 解压EPUB
649
+ if not processor.extract_epub():
650
+ sys.exit(1)
651
+
652
+ # 解析容器文件
653
+ opf_path = processor.parse_container()
654
+ if not opf_path:
655
+ print("无法解析EPUB容器文件")
656
+ sys.exit(1)
657
+
658
+ # 解析OPF文件
659
+ if not processor.parse_opf(opf_path):
660
+ sys.exit(1)
661
+
662
+ # 创建网页界面
663
+ processor.create_web_interface()
664
+
665
+ # 启动Web服务器
666
+ os.chdir(processor.web_dir)
667
+ server_address = ('', args.port)
668
+ httpd = HTTPServer(server_address,
669
+ lambda *x, **y: EPUBHTTPRequestHandler(*x, web_dir=processor.web_dir, **y))
670
+
671
+ print(f"Web服务器已启动: http://localhost:{args.port}")
672
+ print(f"书籍标题: {processor.book_title}")
673
+ print("按 Ctrl+C 停止服务器")
674
+
675
+ # 自动打开浏览器
676
+ if not args.no_browser:
677
+ webbrowser.open(f'http://localhost:{args.port}')
678
+
679
+ # 启动服务器
680
+ httpd.serve_forever()
681
+
682
+ except KeyboardInterrupt:
683
+ print("\n正在关闭服务器...")
684
+ except Exception as e:
685
+ print(f"发生错误: {e}")
686
+ finally:
687
+ processor.cleanup()
688
+
689
+ if __name__ == '__main__':
690
+ main()
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: epub-browser
3
+ Version: 0.1.0
4
+ Summary: A tool to open epub files and serve them via a local web server for reading in a browser.
5
+ Home-page: https://github.com/dfface/epub-browser
6
+ Author: dfface
7
+ Author-email: dfface@sina.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: requires-python
20
+ Dynamic: summary
21
+
22
+ # epub-browser
23
+
24
+ Read epub file in the browser(Chrome/Edge/Safari...).
25
+
26
+ ## Usage
27
+
28
+ Type the command in the terminal:
29
+
30
+ ```bash
31
+ pip install epub-browser
32
+ epub-browser path/to/xxx.epub
33
+ ```
34
+
35
+ Then a browser will be opened to view the epub file.
36
+
37
+ ![epub on web](assets/test1.png)
38
+
39
+ ## Tips
40
+
41
+ You can combine web reading with the web extension called [Circle Reader](https://circlereader.com/) to gain more elegant experience.
42
+
43
+ Other extensions that are recommended are:
44
+
45
+ 1. [Diigo](https://www.diigo.com/): Read more effectively with annotation tools.
46
+ 2. ...
@@ -0,0 +1,9 @@
1
+ README.md
2
+ setup.py
3
+ epub-browser/__init__.py
4
+ epub-browser/main.py
5
+ epub_browser.egg-info/PKG-INFO
6
+ epub_browser.egg-info/SOURCES.txt
7
+ epub_browser.egg-info/dependency_links.txt
8
+ epub_browser.egg-info/entry_points.txt
9
+ epub_browser.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ epub-browser = epub_browser.main:main
@@ -0,0 +1 @@
1
+ epub-browser
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,31 @@
1
+ # epub_browser/setup.py
2
+ from setuptools import setup, find_packages
3
+
4
+ with open("README.md", "r", encoding="utf-8") as fh:
5
+ long_description = fh.read()
6
+
7
+ setup(
8
+ name="epub-browser", # 在PyPI上显示的项目名称
9
+ version="0.1.0", # 初始版本号
10
+ author="dfface", # 作者名
11
+ author_email="dfface@sina.com", # 作者邮箱
12
+ description="A tool to open epub files and serve them via a local web server for reading in a browser.", # 简短描述
13
+ long_description=long_description, # 详细描述,从README.md读取
14
+ long_description_content_type="text/markdown", # 详细描述格式
15
+ url="https://github.com/dfface/epub-browser", # 项目主页,如GitHub仓库地址
16
+ packages=find_packages(), # 自动发现包
17
+ classifiers=[ # 项目分类器,帮助用户找到你的项目
18
+ "Programming Language :: Python :: 3",
19
+ "License :: OSI Approved :: MIT License", # 请根据实际情况选择许可证
20
+ "Operating System :: OS Independent",
21
+ ],
22
+ python_requires='>=3.6', # 指定Python版本要求
23
+ install_requires=[ # 项目依赖的第三方包
24
+ # 例如 "requests", 如果您的工具没有额外依赖,可以留空列表 []
25
+ ],
26
+ entry_points={ # 创建命令行可执行脚本的关键!
27
+ 'console_scripts': [
28
+ 'epub-browser=epub_browser.main:main', # 格式:'命令名=模块路径:函数名'
29
+ ],
30
+ },
31
+ )