Spaces:
Running
Running
| """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() | |
| 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, | |
| ) | |
| def health(): | |
| return jsonify({"ok": True}) | |
| 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}) | |
| def serve_book_image(filename): | |
| return send_file(os.path.join(settings.IMAGES_DIR, filename)) | |
| 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}) | |
| 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) | |
| def get_full_text(book_id): | |
| data = database.get_book_segments(book_id) | |
| if not data: | |
| abort(404, "Book not found") | |
| return jsonify(data) | |
| 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}) | |
| def get_progress(book_id): | |
| progress = database.get_progress(book_id) | |
| if not progress: | |
| abort(404, "No progress found") | |
| return jsonify(progress) | |
| 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") | |
| 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}) | |
| 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}") | |
| 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}") | |
| 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}") | |
| 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}) | |
| 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}) | |
| def delete_folder(folder_id): | |
| database.delete_folder(folder_id) | |
| return jsonify({"ok": True}) | |
| 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}) | |
| def get_bookmarks(book_id): | |
| return jsonify(database.get_bookmarks(book_id)) | |
| 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}) | |
| 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}) | |
| def delete_bookmark(bookmark_id): | |
| database.delete_bookmark(bookmark_id) | |
| return jsonify({"ok": True}) | |
| 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"]}) | |
| 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) | |