"""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/") def serve_book_image(filename): return send_file(os.path.join(settings.IMAGES_DIR, filename)) @app.route("/rename/", 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//page/") 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//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//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//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//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/", 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//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/", methods=["DELETE"]) def delete_folder(folder_id): database.delete_folder(folder_id) return jsonify({"ok": True}) @app.route("/book//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//bookmarks") def get_bookmarks(book_id): return jsonify(database.get_bookmarks(book_id)) @app.route("/book//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//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/", 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)