randusertry commited on
Commit
732702f
·
verified ·
1 Parent(s): 1305752

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +285 -0
app.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flask ebook reader webapp."""
2
+
3
+ import os
4
+ import uuid
5
+ from flask import Flask, request, jsonify, render_template, send_file, abort
6
+
7
+ import settings
8
+ import book_utils
9
+ import llm_service
10
+ import database
11
+ from base import SUPPORTED_LANGUAGES
12
+ from langdetect import detect as langdetect_detect
13
+
14
+ app = Flask(__name__)
15
+ book_utils.ensure_dirs()
16
+ database.init_db()
17
+
18
+
19
+ @app.route("/")
20
+ def index():
21
+ folder_id = request.args.get("folder", None)
22
+ books = database.get_all_books(folder_id)
23
+ folders = database.get_all_folders()
24
+ return render_template("index.html",
25
+ books=books,
26
+ folders=folders,
27
+ current_folder=folder_id,
28
+ languages=SUPPORTED_LANGUAGES,
29
+ )
30
+
31
+ @app.route("/health")
32
+ def health():
33
+ return jsonify({"ok": True})
34
+
35
+
36
+
37
+ @app.route("/upload", methods=["POST"])
38
+ def upload_book():
39
+ file = request.files.get("file")
40
+ if not file or not file.filename:
41
+ abort(400, "No file provided")
42
+
43
+ title = request.form.get("title", "")
44
+ language = request.form.get("language", "auto")
45
+ folder_id = request.form.get("folder_id", "")
46
+
47
+ ext = os.path.splitext(file.filename)[1].lower()
48
+ if ext not in (".pdf", ".epub", ".mobi"):
49
+ abort(400, "Unsupported format. Use .pdf, .epub, or .mobi")
50
+
51
+ book_id = uuid.uuid4().hex[:12]
52
+ if not title:
53
+ title = os.path.splitext(file.filename)[0]
54
+
55
+ filepath = os.path.join(settings.UPLOAD_DIR, f"{book_id}{ext}")
56
+ file.save(filepath)
57
+
58
+ try:
59
+ result = book_utils.parse_book(filepath, book_id, title)
60
+ except Exception as e:
61
+ os.remove(filepath)
62
+ abort(500, f"Failed to parse book: {e}")
63
+
64
+ detected_lang = language
65
+ if language == "auto":
66
+ sample = " ".join(result["pages"][:3])[:2000]
67
+ try:
68
+ code = langdetect_detect(sample)
69
+ supported_codes = {l['code'] for l in SUPPORTED_LANGUAGES}
70
+ detected_lang = code if code in supported_codes else "en"
71
+ except Exception:
72
+ detected_lang = "en"
73
+
74
+ fid = folder_id if folder_id else None
75
+ database.save_book(book_id, title, filepath, result["cover"], detected_lang,
76
+ result["pages"], fid, result.get("segments"), result.get("chapters"))
77
+ return jsonify({"book_id": book_id, "title": title, "num_pages": len(result["pages"]), "language": detected_lang})
78
+
79
+
80
+ @app.route("/book_images/<path:filename>")
81
+ def serve_book_image(filename):
82
+ return send_file(os.path.join(settings.IMAGES_DIR, filename))
83
+
84
+
85
+ @app.route("/rename/<book_id>", methods=["POST"])
86
+ def rename_book_endpoint(book_id):
87
+ book = database.get_book(book_id)
88
+ if not book:
89
+ abort(404, "Book not found")
90
+ title = request.form.get("title")
91
+ if not title:
92
+ abort(400, "Title required")
93
+ database.rename_book(book_id, title)
94
+ if book["cover"] and book["cover"].endswith(".png"):
95
+ new_cover = book_utils.generate_cover(title, book_id)
96
+ database.update_book_cover(book_id, new_cover)
97
+ return jsonify({"ok": True})
98
+
99
+
100
+ @app.route("/book/<book_id>/page/<int:page_num>")
101
+ def get_page(book_id, page_num):
102
+ result = database.get_book_page(book_id, page_num)
103
+ if not result:
104
+ abort(404, "Book or page not found")
105
+ return jsonify(result)
106
+
107
+
108
+ @app.route("/book/<book_id>/full_text")
109
+ def get_full_text(book_id):
110
+ data = database.get_book_segments(book_id)
111
+ if not data:
112
+ abort(404, "Book not found")
113
+ return jsonify(data)
114
+
115
+
116
+ @app.route("/book/<book_id>/progress", methods=["POST"])
117
+ def save_progress(book_id):
118
+ body = request.get_json()
119
+ page = body.get("current_page", 0)
120
+ percent = body.get("percent_read", None)
121
+ if percent is not None:
122
+ database.update_progress_direct(book_id, page, percent)
123
+ else:
124
+ database.update_progress(book_id, page)
125
+ return jsonify({"ok": True})
126
+
127
+
128
+ @app.route("/book/<book_id>/progress")
129
+ def get_progress(book_id):
130
+ progress = database.get_progress(book_id)
131
+ if not progress:
132
+ abort(404, "No progress found")
133
+ return jsonify(progress)
134
+
135
+
136
+ @app.route("/book/<book_id>/cover")
137
+ def get_cover(book_id):
138
+ book = database.get_book(book_id)
139
+ if not book:
140
+ abort(404, "Book not found")
141
+ cover = book["cover"]
142
+ if cover and os.path.isfile(cover):
143
+ return send_file(cover)
144
+ abort(404, "No cover")
145
+
146
+
147
+ @app.route("/book/<book_id>", methods=["DELETE"])
148
+ def delete_book_endpoint(book_id):
149
+ info = database.delete_book(book_id)
150
+ if not info:
151
+ abort(404, "Book not found")
152
+ if info["filepath"] and os.path.isfile(info["filepath"]):
153
+ os.remove(info["filepath"])
154
+ if info["cover"] and os.path.isfile(info["cover"]):
155
+ os.remove(info["cover"])
156
+ return jsonify({"ok": True})
157
+
158
+
159
+ @app.route("/translate", methods=["POST"])
160
+ def translate():
161
+ body = request.get_json()
162
+ text = body.get("text", "")
163
+ lang = body.get("language", "en")
164
+ if not text:
165
+ abort(400, "No text provided")
166
+ try:
167
+ result = llm_service.translate_pages(text, lang)
168
+ return jsonify({"translation": result})
169
+ except Exception as e:
170
+ abort(500, f"Translation failed: {e}")
171
+
172
+
173
+ @app.route("/explain", methods=["POST"])
174
+ def explain():
175
+ body = request.get_json()
176
+ text = body.get("text", "")
177
+ lang = body.get("language", "en")
178
+ if not text:
179
+ abort(400, "No text provided")
180
+ try:
181
+ result = llm_service.explain_selection(text, lang)
182
+ return jsonify({"explanation": result})
183
+ except Exception as e:
184
+ abort(500, f"Explanation failed: {e}")
185
+
186
+
187
+ @app.route("/analyze", methods=["POST"])
188
+ def analyze():
189
+ body = request.get_json()
190
+ text = body.get("text", "")
191
+ lang = body.get("language", "en")
192
+ if not text:
193
+ abort(400, "No text provided")
194
+ try:
195
+ result = llm_service.word_by_word_analysis(text, lang)
196
+ return jsonify(result)
197
+ except Exception as e:
198
+ abort(500, f"Analysis failed: {e}")
199
+
200
+
201
+ @app.route("/folders", methods=["POST"])
202
+ def create_folder():
203
+ body = request.get_json()
204
+ name = body.get("name", "").strip()
205
+ if not name:
206
+ abort(400, "Folder name required")
207
+ folder_id = database.create_folder(name)
208
+ return jsonify({"id": folder_id, "name": name})
209
+
210
+
211
+ @app.route("/folders/<folder_id>/rename", methods=["POST"])
212
+ def rename_folder(folder_id):
213
+ body = request.get_json()
214
+ name = body.get("name", "").strip()
215
+ if not name:
216
+ abort(400, "Folder name required")
217
+ database.rename_folder(folder_id, name)
218
+ return jsonify({"ok": True})
219
+
220
+
221
+ @app.route("/folders/<folder_id>", methods=["DELETE"])
222
+ def delete_folder(folder_id):
223
+ database.delete_folder(folder_id)
224
+ return jsonify({"ok": True})
225
+
226
+
227
+ @app.route("/book/<book_id>/move", methods=["POST"])
228
+ def move_book(book_id):
229
+ body = request.get_json()
230
+ folder_id = body.get("folder_id", None)
231
+ database.move_book_to_folder(book_id, folder_id)
232
+ return jsonify({"ok": True})
233
+
234
+
235
+ @app.route("/book/<book_id>/bookmarks")
236
+ def get_bookmarks(book_id):
237
+ return jsonify(database.get_bookmarks(book_id))
238
+
239
+
240
+ @app.route("/book/<book_id>/bookmarks", methods=["POST"])
241
+ def add_bookmark(book_id):
242
+ body = request.get_json()
243
+ name = body.get("name", "").strip()
244
+ segment_index = body.get("segment_index", 0)
245
+ if not name:
246
+ abort(400, "Bookmark name required")
247
+ bm_id = database.add_bookmark(book_id, name, segment_index)
248
+ return jsonify({"id": bm_id, "name": name, "segment_index": segment_index})
249
+
250
+
251
+ @app.route("/bookmarks/<int:bookmark_id>/rename", methods=["POST"])
252
+ def rename_bookmark(bookmark_id):
253
+ body = request.get_json()
254
+ name = body.get("name", "").strip()
255
+ if not name:
256
+ abort(400, "Name required")
257
+ database.rename_bookmark(bookmark_id, name)
258
+ return jsonify({"ok": True})
259
+
260
+
261
+ @app.route("/bookmarks/<int:bookmark_id>", methods=["DELETE"])
262
+ def delete_bookmark(bookmark_id):
263
+ database.delete_bookmark(bookmark_id)
264
+ return jsonify({"ok": True})
265
+
266
+
267
+ @app.route("/last-read")
268
+ def last_read():
269
+ book = database.get_last_read_book()
270
+ if not book:
271
+ return jsonify({"book_id": None})
272
+ return jsonify({"book_id": book["id"], "percent_read": book["percent_read"]})
273
+
274
+
275
+ @app.route("/languages")
276
+ def get_languages():
277
+ return jsonify(SUPPORTED_LANGUAGES)
278
+
279
+
280
+ # ASGI wrapper so uvicorn can serve this Flask app
281
+ from asgiref.wsgi import WsgiToAsgi
282
+ asgi_app = WsgiToAsgi(app)
283
+
284
+ if __name__ == "__main__":
285
+ app.run(debug=True, host="0.0.0.0", port=8000)