Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ wheels/
# Custom
*_data/
*.epub

# Library
library/
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ dependencies = [
"ebooklib>=0.20",
"fastapi>=0.121.2",
"jinja2>=3.1.6",
"python-multipart>=0.0.6",
"uvicorn>=0.38.0",
]
18 changes: 16 additions & 2 deletions reader3.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ class ChapterContent:
content: str # Cleaned HTML with rewritten image paths
text: str # Plain text for search/LLM context
order: int # Linear reading order
id: str
href: str
title: str
content: str
text: str
order: int


@dataclass
Expand Down Expand Up @@ -300,8 +306,16 @@ def save_to_pickle(book: Book, output_dir: str):
sys.exit(1)

epub_file = sys.argv[1]
assert os.path.exists(epub_file), "File not found."
out_dir = os.path.splitext(epub_file)[0] + "_data"
assert os.path.exists(epub_file), f"File not found: {epub_file}"

library_dir = "library"
if not os.path.exists(library_dir):
os.makedirs(library_dir)
print(f"Created library directory: {library_dir}")

# Extract book name from EPUB file (without path and extension)
book_basename = os.path.splitext(os.path.basename(epub_file))[0]
out_dir = os.path.join(library_dir, book_basename + "_data")

book_obj = process_epub(epub_file, out_dir)
save_to_pickle(book_obj, out_dir)
Expand Down
76 changes: 69 additions & 7 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
from functools import lru_cache
from typing import Optional

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi import FastAPI, Request, HTTPException, UploadFile, File
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse
from fastapi.templating import Jinja2Templates

from reader3 import Book, BookMetadata, ChapterContent, TOCEntry
from reader3 import Book, BookMetadata, ChapterContent, TOCEntry, process_epub, save_to_pickle

app = FastAPI()
templates = Jinja2Templates(directory="templates")

# Where are the book folders located?
BOOKS_DIR = "."
BOOKS_DIR = "library"

if not os.path.exists(BOOKS_DIR):
os.makedirs(BOOKS_DIR)
print(f"Created library directory: {BOOKS_DIR}")

@lru_cache(maxsize=10)
def load_book_cached(folder_name: str) -> Optional[Book]:
Expand All @@ -27,22 +30,38 @@ def load_book_cached(folder_name: str) -> Optional[Book]:
return None

try:
# Ensure Book class is available in the pickle context
import sys
import reader3
# Make Book available as both __main__.Book and reader3.Book for pickle compatibility
if '__main__' not in sys.modules or not hasattr(sys.modules['__main__'], 'Book'):
import types
if '__main__' not in sys.modules:
sys.modules['__main__'] = types.ModuleType('__main__')
sys.modules['__main__'].Book = Book
sys.modules['__main__'].BookMetadata = BookMetadata
sys.modules['__main__'].ChapterContent = ChapterContent
sys.modules['__main__'].TOCEntry = TOCEntry

with open(file_path, "rb") as f:
book = pickle.load(f)
return book
except Exception as e:
print(f"Error loading book {folder_name}: {e}")
import traceback
traceback.print_exc()
return None

@app.get("/", response_class=HTMLResponse)
async def library_view(request: Request):
"""Lists all available processed books."""
books = []

# Scan directory for folders ending in '_data' that have a book.pkl
# Scan library directory for folders ending in '_data' that have a book.pkl
if os.path.exists(BOOKS_DIR):
for item in os.listdir(BOOKS_DIR):
if item.endswith("_data") and os.path.isdir(item):
item_path = os.path.join(BOOKS_DIR, item)
if item.endswith("_data") and os.path.isdir(item_path):
# Try to load it to get the title
book = load_book_cached(item)
if book:
Expand Down Expand Up @@ -104,6 +123,49 @@ async def serve_image(book_id: str, image_name: str):

return FileResponse(img_path)

@app.post("/import", response_class=HTMLResponse)
async def import_book(request: Request, file: UploadFile = File(...)):
"""Import an EPUB file and process it."""
# Validate file extension
if not file.filename.endswith('.epub'):
raise HTTPException(status_code=400, detail="Only EPUB files are supported")

# Create library directory if it doesn't exist
if not os.path.exists(BOOKS_DIR):
os.makedirs(BOOKS_DIR)

# Save uploaded file temporarily
temp_path = os.path.join(BOOKS_DIR, file.filename)
try:
with open(temp_path, "wb") as f:
content = await file.read()
f.write(content)

# Process the EPUB
book_basename = os.path.splitext(file.filename)[0]
out_dir = os.path.join(BOOKS_DIR, book_basename + "_data")

print(f"Processing {file.filename}...")
book_obj = process_epub(temp_path, out_dir)
save_to_pickle(book_obj, out_dir)

# Clean up temporary EPUB file
os.remove(temp_path)

# Clear cache so new book appears
load_book_cached.cache_clear()

print(f"Successfully imported: {book_obj.metadata.title}")

# Redirect to library
return RedirectResponse(url="/", status_code=303)

except Exception as e:
# Clean up on error
if os.path.exists(temp_path):
os.remove(temp_path)
raise HTTPException(status_code=500, detail=f"Error importing book: {str(e)}")

if __name__ == "__main__":
import uvicorn
print("Starting server at http://127.0.0.1:8123")
Expand Down
11 changes: 10 additions & 1 deletion templates/library.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,17 @@
<div class="container">
<h1>Library</h1>

<div style="margin-bottom: 30px; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<h2 style="margin-top: 0; font-size: 1.1em; color: #333;">Import Book</h2>
<form action="/import" method="post" enctype="multipart/form-data" style="display: flex; gap: 10px; align-items: center;">
<input type="file" name="file" accept=".epub" required style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<button type="submit" class="btn" style="padding: 8px 20px; border: none; cursor: pointer;">Import EPUB</button>
</form>
<p style="margin-top: 10px; margin-bottom: 0; color: #666; font-size: 0.9em;">Or use the command line: <code>uv run reader3.py &lt;book.epub&gt;</code></p>
</div>

{% if not books %}
<p>No processed books found. Run <code>reader3.py</code> on an epub first.</p>
<p>No processed books found. Import an EPUB file above or run <code>reader3.py</code> on an epub file.</p>
{% endif %}

<div class="book-grid">
Expand Down
11 changes: 11 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.