book_reader_app / app.py
randusertry's picture
Create app.py
732702f verified
"""Flask ebook reader webapp."""
import os
import uuid
from flask import Flask, request, jsonify, render_template, send_file, abort
import settings
import book_utils
import llm_service
import database
from base import SUPPORTED_LANGUAGES
from langdetect import detect as langdetect_detect
app = Flask(__name__)
book_utils.ensure_dirs()
database.init_db()
@app.route("/")
def index():
folder_id = request.args.get("folder", None)
books = database.get_all_books(folder_id)
folders = database.get_all_folders()
return render_template("index.html",
books=books,
folders=folders,
current_folder=folder_id,
languages=SUPPORTED_LANGUAGES,
)
@app.route("/health")
def health():
return jsonify({"ok": True})
@app.route("/upload", methods=["POST"])
def upload_book():
file = request.files.get("file")
if not file or not file.filename:
abort(400, "No file provided")
title = request.form.get("title", "")
language = request.form.get("language", "auto")
folder_id = request.form.get("folder_id", "")
ext = os.path.splitext(file.filename)[1].lower()
if ext not in (".pdf", ".epub", ".mobi"):
abort(400, "Unsupported format. Use .pdf, .epub, or .mobi")
book_id = uuid.uuid4().hex[:12]
if not title:
title = os.path.splitext(file.filename)[0]
filepath = os.path.join(settings.UPLOAD_DIR, f"{book_id}{ext}")
file.save(filepath)
try:
result = book_utils.parse_book(filepath, book_id, title)
except Exception as e:
os.remove(filepath)
abort(500, f"Failed to parse book: {e}")
detected_lang = language
if language == "auto":
sample = " ".join(result["pages"][:3])[:2000]
try:
code = langdetect_detect(sample)
supported_codes = {l['code'] for l in SUPPORTED_LANGUAGES}
detected_lang = code if code in supported_codes else "en"
except Exception:
detected_lang = "en"
fid = folder_id if folder_id else None
database.save_book(book_id, title, filepath, result["cover"], detected_lang,
result["pages"], fid, result.get("segments"), result.get("chapters"))
return jsonify({"book_id": book_id, "title": title, "num_pages": len(result["pages"]), "language": detected_lang})
@app.route("/book_images/<path:filename>")
def serve_book_image(filename):
return send_file(os.path.join(settings.IMAGES_DIR, filename))
@app.route("/rename/<book_id>", methods=["POST"])
def rename_book_endpoint(book_id):
book = database.get_book(book_id)
if not book:
abort(404, "Book not found")
title = request.form.get("title")
if not title:
abort(400, "Title required")
database.rename_book(book_id, title)
if book["cover"] and book["cover"].endswith(".png"):
new_cover = book_utils.generate_cover(title, book_id)
database.update_book_cover(book_id, new_cover)
return jsonify({"ok": True})
@app.route("/book/<book_id>/page/<int:page_num>")
def get_page(book_id, page_num):
result = database.get_book_page(book_id, page_num)
if not result:
abort(404, "Book or page not found")
return jsonify(result)
@app.route("/book/<book_id>/full_text")
def get_full_text(book_id):
data = database.get_book_segments(book_id)
if not data:
abort(404, "Book not found")
return jsonify(data)
@app.route("/book/<book_id>/progress", methods=["POST"])
def save_progress(book_id):
body = request.get_json()
page = body.get("current_page", 0)
percent = body.get("percent_read", None)
if percent is not None:
database.update_progress_direct(book_id, page, percent)
else:
database.update_progress(book_id, page)
return jsonify({"ok": True})
@app.route("/book/<book_id>/progress")
def get_progress(book_id):
progress = database.get_progress(book_id)
if not progress:
abort(404, "No progress found")
return jsonify(progress)
@app.route("/book/<book_id>/cover")
def get_cover(book_id):
book = database.get_book(book_id)
if not book:
abort(404, "Book not found")
cover = book["cover"]
if cover and os.path.isfile(cover):
return send_file(cover)
abort(404, "No cover")
@app.route("/book/<book_id>", methods=["DELETE"])
def delete_book_endpoint(book_id):
info = database.delete_book(book_id)
if not info:
abort(404, "Book not found")
if info["filepath"] and os.path.isfile(info["filepath"]):
os.remove(info["filepath"])
if info["cover"] and os.path.isfile(info["cover"]):
os.remove(info["cover"])
return jsonify({"ok": True})
@app.route("/translate", methods=["POST"])
def translate():
body = request.get_json()
text = body.get("text", "")
lang = body.get("language", "en")
if not text:
abort(400, "No text provided")
try:
result = llm_service.translate_pages(text, lang)
return jsonify({"translation": result})
except Exception as e:
abort(500, f"Translation failed: {e}")
@app.route("/explain", methods=["POST"])
def explain():
body = request.get_json()
text = body.get("text", "")
lang = body.get("language", "en")
if not text:
abort(400, "No text provided")
try:
result = llm_service.explain_selection(text, lang)
return jsonify({"explanation": result})
except Exception as e:
abort(500, f"Explanation failed: {e}")
@app.route("/analyze", methods=["POST"])
def analyze():
body = request.get_json()
text = body.get("text", "")
lang = body.get("language", "en")
if not text:
abort(400, "No text provided")
try:
result = llm_service.word_by_word_analysis(text, lang)
return jsonify(result)
except Exception as e:
abort(500, f"Analysis failed: {e}")
@app.route("/folders", methods=["POST"])
def create_folder():
body = request.get_json()
name = body.get("name", "").strip()
if not name:
abort(400, "Folder name required")
folder_id = database.create_folder(name)
return jsonify({"id": folder_id, "name": name})
@app.route("/folders/<folder_id>/rename", methods=["POST"])
def rename_folder(folder_id):
body = request.get_json()
name = body.get("name", "").strip()
if not name:
abort(400, "Folder name required")
database.rename_folder(folder_id, name)
return jsonify({"ok": True})
@app.route("/folders/<folder_id>", methods=["DELETE"])
def delete_folder(folder_id):
database.delete_folder(folder_id)
return jsonify({"ok": True})
@app.route("/book/<book_id>/move", methods=["POST"])
def move_book(book_id):
body = request.get_json()
folder_id = body.get("folder_id", None)
database.move_book_to_folder(book_id, folder_id)
return jsonify({"ok": True})
@app.route("/book/<book_id>/bookmarks")
def get_bookmarks(book_id):
return jsonify(database.get_bookmarks(book_id))
@app.route("/book/<book_id>/bookmarks", methods=["POST"])
def add_bookmark(book_id):
body = request.get_json()
name = body.get("name", "").strip()
segment_index = body.get("segment_index", 0)
if not name:
abort(400, "Bookmark name required")
bm_id = database.add_bookmark(book_id, name, segment_index)
return jsonify({"id": bm_id, "name": name, "segment_index": segment_index})
@app.route("/bookmarks/<int:bookmark_id>/rename", methods=["POST"])
def rename_bookmark(bookmark_id):
body = request.get_json()
name = body.get("name", "").strip()
if not name:
abort(400, "Name required")
database.rename_bookmark(bookmark_id, name)
return jsonify({"ok": True})
@app.route("/bookmarks/<int:bookmark_id>", methods=["DELETE"])
def delete_bookmark(bookmark_id):
database.delete_bookmark(bookmark_id)
return jsonify({"ok": True})
@app.route("/last-read")
def last_read():
book = database.get_last_read_book()
if not book:
return jsonify({"book_id": None})
return jsonify({"book_id": book["id"], "percent_read": book["percent_read"]})
@app.route("/languages")
def get_languages():
return jsonify(SUPPORTED_LANGUAGES)
# ASGI wrapper so uvicorn can serve this Flask app
from asgiref.wsgi import WsgiToAsgi
asgi_app = WsgiToAsgi(app)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=8000)