-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathapp.py
239 lines (203 loc) · 7.64 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"""Flask web application for book download service with URL rewrite support."""
import logging
import io, re, os
from flask import Flask, request, jsonify, render_template, send_file, send_from_directory
from werkzeug.middleware.proxy_fix import ProxyFix
from flask import url_for as flask_url_for
from functools import partial
from logger import setup_logger
from config import FLASK_HOST, FLASK_PORT, FLASK_DEBUG
import backend
logger = setup_logger(__name__)
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app) # type: ignore
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching
app.config['APPLICATION_ROOT'] = '/'
# Flask logger
app.logger.handlers = logger.handlers
app.logger.setLevel(logger.level)
# Also handle Werkzeug's logger
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.handlers = logger.handlers
werkzeug_logger.setLevel(logger.level)
def register_dual_routes(app):
"""
Register each route both with and without the /request prefix.
This function should be called after all routes are defined.
"""
# Store original url_map rules
rules = list(app.url_map.iter_rules())
# Add /request prefix to each rule
for rule in rules:
if rule.rule != '/request/' and rule.rule != '/request': # Skip if it's already a request route
# Create new routes with /request prefix, both with and without trailing slash
base_rule = rule.rule[:-1] if rule.rule.endswith('/') else rule.rule
if base_rule == '': # Special case for root path
app.add_url_rule('/request', f"root_request",
view_func=app.view_functions[rule.endpoint],
methods=rule.methods)
app.add_url_rule('/request/', f"root_request_slash",
view_func=app.view_functions[rule.endpoint],
methods=rule.methods)
else:
app.add_url_rule(f"/request{base_rule}",
f"{rule.endpoint}_request",
view_func=app.view_functions[rule.endpoint],
methods=rule.methods)
app.add_url_rule(f"/request{base_rule}/",
f"{rule.endpoint}_request_slash",
view_func=app.view_functions[rule.endpoint],
methods=rule.methods)
app.jinja_env.globals['url_for'] = url_for_with_request
def url_for_with_request(endpoint, **values):
"""Generate URLs with /request prefix by default."""
if endpoint == 'static':
# For static files, add /request prefix
url = flask_url_for(endpoint, **values)
return f"/request{url}"
return flask_url_for(endpoint, **values)
@app.route('/')
def index():
"""
Render main page with search and status table.
"""
return render_template('index.html')
@app.route('/favico<path:_>')
@app.route('/request/favico<path:_>')
@app.route('/request/static/favico<path:_>')
def favicon(_):
return send_from_directory(os.path.join(app.root_path, 'static', 'media'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/api/search', methods=['GET'])
def api_search():
"""
Search for books matching the provided query.
Query Parameters:
query (str): Search term (ISBN, title, author, etc.)
Returns:
flask.Response: JSON array of matching books or empty array if no query.
"""
query = request.args.get('query', '')
if not query:
return jsonify([])
try:
books = backend.search_books(query)
return jsonify(books)
except Exception as e:
logger.error(f"Search error: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/info', methods=['GET'])
def api_info():
"""
Get detailed book information.
Query Parameters:
id (str): Book identifier (MD5 hash)
Returns:
flask.Response: JSON object with book details, or an error message.
"""
book_id = request.args.get('id', '')
if not book_id:
return jsonify({"error": "No book ID provided"}), 400
try:
book = backend.get_book_info(book_id)
if book:
return jsonify(book)
return jsonify({"error": "Book not found"}), 404
except Exception as e:
logger.error(f"Info error: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/download', methods=['GET'])
def api_download():
"""
Queue a book for download.
Query Parameters:
id (str): Book identifier (MD5 hash)
Returns:
flask.Response: JSON status object indicating success or failure.
"""
book_id = request.args.get('id', '')
if not book_id:
return jsonify({"error": "No book ID provided"}), 400
try:
success = backend.queue_book(book_id)
if success:
return jsonify({"status": "queued"})
return jsonify({"error": "Failed to queue book"}), 500
except Exception as e:
logger.error(f"Download error: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/status', methods=['GET'])
def api_status():
"""
Get current download queue status.
Returns:
flask.Response: JSON object with queue status.
"""
try:
status = backend.queue_status()
return jsonify(status)
except Exception as e:
logger.error(f"Status error: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/localdownload', methods=['GET'])
def api_local_download():
"""
Download an EPUB file from local storage if available.
Query Parameters:
id (str): Book identifier (MD5 hash)
Returns:
flask.Response: The EPUB file if found, otherwise an error response.
"""
book_id = request.args.get('id', '')
if not book_id:
return jsonify({"error": "No book ID provided"}), 400
try:
file_data, file_name = backend.get_book_data(book_id)
if file_data is None:
# Book data not found or not available
return jsonify({"error": "File not found"}), 404
# Santize the file name
file_name = re.sub(r'[\\/:*?"<>|]', '_', file_name.strip())[:255]
# Prepare the file for sending to the client
epub_file = io.BytesIO(file_data)
# Typically EPUB mime-type: 'application/epub+zip'
return send_file(
epub_file,
mimetype='application/epub+zip',
download_name=f"{file_name}.epub",
as_attachment=True
)
except Exception as e:
logger.error(f"Local download error: {e}")
return jsonify({"error": str(e)}), 500
@app.errorhandler(404)
def not_found_error(error):
"""
Handle 404 (Not Found) errors.
Args:
error (HTTPException): The 404 error raised by Flask.
Returns:
flask.Response: JSON error message with 404 status.
"""
logger.warning(f"404 error: {request.url}")
return jsonify({"error": "Resource not found"}), 404
@app.errorhandler(500)
def internal_error(error):
"""
Handle 500 (Internal Server) errors.
Args:
error (HTTPException): The 500 error raised by Flask.
Returns:
flask.Response: JSON error message with 500 status.
"""
logger.error(f"500 error: {error}")
return jsonify({"error": "Internal server error"}), 500
if __name__ == '__main__':
# Register all routes with /request prefix
register_dual_routes(app)
logger.info(f"Starting Flask application on {FLASK_HOST}:{FLASK_PORT}")
app.run(
host=FLASK_HOST,
port=FLASK_PORT,
debug=FLASK_DEBUG # Disable debug mode in production
)