82 lines
2.2 KiB
Python
82 lines
2.2 KiB
Python
|
|
"""EPUB parsing: turn a .epub file into a spine-ordered list of chapters."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import hashlib
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import ebooklib
|
||
|
|
from bs4 import BeautifulSoup
|
||
|
|
from ebooklib import epub
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Chapter:
|
||
|
|
title: str
|
||
|
|
paragraphs: list[str]
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Book:
|
||
|
|
id: str
|
||
|
|
title: str
|
||
|
|
author: str
|
||
|
|
path: Path
|
||
|
|
chapters: list[Chapter]
|
||
|
|
|
||
|
|
|
||
|
|
def _book_id(path: Path) -> str:
|
||
|
|
# Identifies a book by its resolved path for now. Once the sync API exists,
|
||
|
|
# this should switch to whatever stable id diora assigns server-side.
|
||
|
|
return hashlib.sha256(str(path.resolve()).encode()).hexdigest()[:16]
|
||
|
|
|
||
|
|
|
||
|
|
def _extract_paragraphs(html: bytes) -> list[str]:
|
||
|
|
soup = BeautifulSoup(html, "html.parser")
|
||
|
|
for tag in soup(["script", "style"]):
|
||
|
|
tag.decompose()
|
||
|
|
|
||
|
|
paragraphs = []
|
||
|
|
for el in soup.find_all(["p", "h1", "h2", "h3", "h4", "li", "blockquote"]):
|
||
|
|
text = el.get_text(" ", strip=True)
|
||
|
|
if text:
|
||
|
|
paragraphs.append(text)
|
||
|
|
|
||
|
|
if not paragraphs:
|
||
|
|
text = soup.get_text(" ", strip=True)
|
||
|
|
if text:
|
||
|
|
paragraphs.append(text)
|
||
|
|
|
||
|
|
return paragraphs
|
||
|
|
|
||
|
|
|
||
|
|
def load_epub(path: Path) -> Book:
|
||
|
|
raw = epub.read_epub(str(path), options={"ignore_ncx": True})
|
||
|
|
|
||
|
|
title_meta = raw.get_metadata("DC", "title")
|
||
|
|
title = title_meta[0][0] if title_meta else path.stem
|
||
|
|
|
||
|
|
author_meta = raw.get_metadata("DC", "creator")
|
||
|
|
author = author_meta[0][0] if author_meta else "Unbekannt"
|
||
|
|
|
||
|
|
chapters: list[Chapter] = []
|
||
|
|
for idref, linear in raw.spine:
|
||
|
|
if linear == "no":
|
||
|
|
continue
|
||
|
|
item = raw.get_item_with_id(idref)
|
||
|
|
if item is None or item.get_type() != ebooklib.ITEM_DOCUMENT:
|
||
|
|
continue
|
||
|
|
paragraphs = _extract_paragraphs(item.get_content())
|
||
|
|
if not paragraphs:
|
||
|
|
continue
|
||
|
|
chapters.append(Chapter(title=paragraphs[0][:60], paragraphs=paragraphs))
|
||
|
|
|
||
|
|
return Book(id=_book_id(path), title=title, author=author, path=path, chapters=chapters)
|
||
|
|
|
||
|
|
|
||
|
|
def scan_library(library_dir: Path) -> list[Path]:
|
||
|
|
if not library_dir.exists():
|
||
|
|
return []
|
||
|
|
return sorted(library_dir.rglob("*.epub"))
|