diff --git a/.gitignore b/.gitignore index 9e1d25d..ee2d2ea 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ wheels/ # Custom *_data/ *.epub + +# Library +library/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 31e6179..b63533e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] diff --git a/reader3.py b/reader3.py index d0b9d3f..4b24668 100644 --- a/reader3.py +++ b/reader3.py @@ -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 @@ -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) diff --git a/server.py b/server.py index 9c870dc..323ef69 100644 --- a/server.py +++ b/server.py @@ -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]: @@ -27,11 +30,26 @@ 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) @@ -39,10 +57,11 @@ 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: @@ -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") diff --git a/templates/library.html b/templates/library.html index e7d094d..f706c17 100644 --- a/templates/library.html +++ b/templates/library.html @@ -20,8 +20,17 @@

Library

+
+

Import Book

+
+ + +
+

Or use the command line: uv run reader3.py <book.epub>

+
+ {% if not books %} -

No processed books found. Run reader3.py on an epub first.

+

No processed books found. Import an EPUB file above or run reader3.py on an epub file.

{% endif %}
diff --git a/uv.lock b/uv.lock index e2e2f80..eaab8e6 100644 --- a/uv.lock +++ b/uv.lock @@ -481,6 +481,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + [[package]] name = "reader3" version = "0.1.0" @@ -490,6 +499,7 @@ dependencies = [ { name = "ebooklib" }, { name = "fastapi" }, { name = "jinja2" }, + { name = "python-multipart" }, { name = "uvicorn" }, ] @@ -499,6 +509,7 @@ requires-dist = [ { name = "ebooklib", specifier = ">=0.20" }, { name = "fastapi", specifier = ">=0.121.2" }, { name = "jinja2", specifier = ">=3.1.6" }, + { name = "python-multipart", specifier = ">=0.0.6" }, { name = "uvicorn", specifier = ">=0.38.0" }, ]