sivakumar330 commited on
Commit
1622e95
·
verified ·
1 Parent(s): f58a5f3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +715 -137
app.py CHANGED
@@ -1,256 +1,834 @@
1
- # app.py - Production Ready eBook API (HuggingFace Compatible)
2
 
3
  import os
4
  import uuid
5
  import datetime
6
- import mimetypes
7
- from flask import Flask, request, jsonify, send_file
8
  from flask_cors import CORS
9
  from werkzeug.utils import secure_filename
 
10
  import json
11
  from pathlib import Path
12
 
13
  app = Flask(__name__)
14
- CORS(app)
15
 
 
16
  UPLOAD_FOLDER = 'uploads'
17
- METADATA_FILE = 'ebooks_metadata.json'
18
  ALLOWED_EXTENSIONS = {'pdf', 'epub', 'txt', 'docx'}
19
- MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
20
 
 
21
  Path(UPLOAD_FOLDER).mkdir(parents=True, exist_ok=True)
22
 
23
-
24
- # -------------------- UTILITIES --------------------
25
 
26
  def load_metadata():
 
27
  if os.path.exists(METADATA_FILE):
28
- with open(METADATA_FILE, 'r') as f:
29
- return json.load(f)
 
 
 
30
  return {}
31
 
32
- def save_metadata(data):
33
- with open(METADATA_FILE, 'w') as f:
34
- json.dump(data, f, indent=2)
 
 
 
 
35
 
36
  def allowed_file(filename):
 
37
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- # -------------------- UPLOAD --------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  @app.route('/api/upload', methods=['POST'])
43
  def upload_ebook():
 
44
  try:
 
45
  if 'file' not in request.files:
46
  return jsonify({'error': 'No file provided'}), 400
47
-
48
  file = request.files['file']
49
-
50
  if file.filename == '':
51
  return jsonify({'error': 'No file selected'}), 400
52
-
53
  if not allowed_file(file.filename):
54
- return jsonify({'error': 'Invalid file type'}), 400
55
-
56
- # 🔥 IMPORTANT FIX — read image_url + price
57
  title = request.form.get('title', '')
58
  author = request.form.get('author', '')
59
  category = request.form.get('category', 'General')
60
  uploaded_by = request.form.get('uploaded_by', '')
61
  image_url = request.form.get('image_url', '')
62
- price = request.form.get('price', 0)
63
-
64
  if not uploaded_by:
65
  return jsonify({'error': 'User ID required'}), 400
66
-
 
67
  original_filename = secure_filename(file.filename)
68
  file_ext = original_filename.rsplit('.', 1)[1].lower()
69
  unique_id = str(uuid.uuid4())
70
- stored_filename = f"{unique_id}.{file_ext}"
71
- filepath = os.path.join(UPLOAD_FOLDER, stored_filename)
72
-
 
73
  file.save(filepath)
74
-
75
  file_size = os.path.getsize(filepath)
 
 
76
  if file_size > MAX_FILE_SIZE:
77
  os.remove(filepath)
78
- return jsonify({'error': 'File too large (max 50MB)'}), 400
79
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  ebooks_metadata = load_metadata()
81
-
82
- ebook_data = {
83
- "id": unique_id,
84
- "filename": original_filename,
85
- "stored_filename": stored_filename,
86
- "title": title or original_filename,
87
- "author": author,
88
- "category": category,
89
- "uploaded_by": uploaded_by,
90
- "image_url": image_url, # ✅ FIXED
91
- "price": int(price) if str(price).isdigit() else 0,
92
- "size": file_size,
93
- "upload_date": datetime.datetime.now().isoformat(),
94
- "download_count": 0
95
- }
96
-
97
- ebooks_metadata[unique_id] = ebook_data
98
  save_metadata(ebooks_metadata)
99
-
100
  return jsonify({
101
- "message": "Upload successful",
102
- "ebook": ebook_data
103
  }), 201
104
-
105
  except Exception as e:
106
- return jsonify({"error": str(e)}), 500
107
-
108
-
109
- # -------------------- GET ALL --------------------
110
 
111
  @app.route('/api/ebooks', methods=['GET'])
112
  def get_ebooks():
 
113
  try:
114
  ebooks_metadata = load_metadata()
115
- ebook_list = list(ebooks_metadata.values())
116
-
117
- ebook_list.sort(
118
- key=lambda x: x.get('upload_date', ''),
119
- reverse=True
120
- )
121
-
 
 
 
122
  return jsonify(ebook_list), 200
123
-
124
  except Exception as e:
125
- return jsonify({"error": str(e)}), 500
126
-
127
-
128
- # -------------------- DOWNLOAD --------------------
129
 
130
  @app.route('/api/download/<ebook_id>', methods=['GET'])
131
  def download_ebook(ebook_id):
 
132
  try:
133
  ebooks_metadata = load_metadata()
134
-
135
  if ebook_id not in ebooks_metadata:
136
- return jsonify({'error': 'Not found'}), 404
137
-
138
- ebook = ebooks_metadata[ebook_id]
139
- filepath = os.path.join(UPLOAD_FOLDER, ebook['stored_filename'])
140
-
141
  if not os.path.exists(filepath):
142
- return jsonify({'error': 'File missing'}), 404
143
-
144
  # Increment download count
145
- ebook['download_count'] += 1
146
  save_metadata(ebooks_metadata)
147
-
148
- mime_type, _ = mimetypes.guess_type(filepath)
149
-
150
  return send_file(
151
  filepath,
152
  as_attachment=True,
153
- download_name=ebook['filename'],
154
- mimetype=mime_type or 'application/octet-stream'
155
  )
156
-
157
  except Exception as e:
158
- return jsonify({"error": str(e)}), 500
159
-
160
-
161
- # -------------------- DELETE --------------------
162
 
163
  @app.route('/api/ebooks/<ebook_id>', methods=['DELETE'])
164
  def delete_ebook(ebook_id):
 
165
  try:
166
  data = request.get_json()
167
  user_id = data.get('user_id')
168
-
169
  if not user_id:
170
  return jsonify({'error': 'User ID required'}), 400
171
-
172
  ebooks_metadata = load_metadata()
173
-
174
  if ebook_id not in ebooks_metadata:
175
- return jsonify({'error': 'Not found'}), 404
176
-
177
- ebook = ebooks_metadata[ebook_id]
178
-
179
- if ebook['uploaded_by'] != user_id:
180
- return jsonify({'error': 'Unauthorized'}), 403
181
-
182
- filepath = os.path.join(UPLOAD_FOLDER, ebook['stored_filename'])
183
-
 
184
  if os.path.exists(filepath):
185
  os.remove(filepath)
186
-
 
187
  del ebooks_metadata[ebook_id]
188
  save_metadata(ebooks_metadata)
189
-
190
- return jsonify({'message': 'Deleted'}), 200
191
-
192
  except Exception as e:
193
- return jsonify({"error": str(e)}), 500
194
-
195
-
196
- # -------------------- SEARCH --------------------
197
 
198
  @app.route('/api/ebooks/search', methods=['GET'])
199
  def search_ebooks():
 
200
  try:
201
  query = request.args.get('q', '').lower()
202
- category = request.args.get('category', '').lower()
203
-
 
 
 
204
  ebooks_metadata = load_metadata()
205
  results = []
206
-
207
- for ebook in ebooks_metadata.values():
208
  match = True
209
-
210
  if query:
211
  match = match and (
212
- query in ebook.get('title', '').lower() or
213
- query in ebook.get('author', '').lower() or
214
- query in ebook.get('filename', '').lower()
215
  )
216
-
217
  if category:
218
- match = match and category == ebook.get('category', '').lower()
219
-
220
  if match:
221
- results.append(ebook)
222
-
 
 
223
  return jsonify(results), 200
224
-
225
  except Exception as e:
226
- return jsonify({"error": str(e)}), 500
227
-
228
-
229
- # -------------------- USER EBOOKS --------------------
230
 
231
  @app.route('/api/ebooks/user/<user_id>', methods=['GET'])
232
  def get_user_ebooks(user_id):
 
233
  try:
234
  ebooks_metadata = load_metadata()
235
- user_ebooks = [
236
- ebook for ebook in ebooks_metadata.values()
237
- if ebook.get('uploaded_by') == user_id
238
- ]
 
 
 
 
239
  return jsonify(user_ebooks), 200
240
-
241
  except Exception as e:
242
- return jsonify({"error": str(e)}), 500
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
- # -------------------- HEALTH --------------------
 
 
 
246
 
247
- @app.route('/api/health', methods=['GET'])
248
- def health():
249
- return jsonify({
250
- "status": "healthy",
251
- "time": datetime.datetime.now().isoformat()
252
- }), 200
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  if __name__ == '__main__':
256
- app.run(host='0.0.0.0', port=7860)
 
 
 
1
+ # app.py - Main Flask application for eBook management with Hugging Face Spaces support
2
 
3
  import os
4
  import uuid
5
  import datetime
6
+ from flask import Flask, request, jsonify, send_file, render_template_string
 
7
  from flask_cors import CORS
8
  from werkzeug.utils import secure_filename
9
+ import hashlib
10
  import json
11
  from pathlib import Path
12
 
13
  app = Flask(__name__)
14
+ CORS(app) # Enable CORS for frontend communication
15
 
16
+ # Configuration
17
  UPLOAD_FOLDER = 'uploads'
 
18
  ALLOWED_EXTENSIONS = {'pdf', 'epub', 'txt', 'docx'}
19
+ MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB limit
20
 
21
+ # Create upload folder if it doesn't exist
22
  Path(UPLOAD_FOLDER).mkdir(parents=True, exist_ok=True)
23
 
24
+ # Persistent storage using JSON file (works on Hugging Face)
25
+ METADATA_FILE = 'ebooks_metadata.json'
26
 
27
  def load_metadata():
28
+ """Load eBook metadata from JSON file"""
29
  if os.path.exists(METADATA_FILE):
30
+ try:
31
+ with open(METADATA_FILE, 'r', encoding='utf-8') as f:
32
+ return json.load(f)
33
+ except:
34
+ return {}
35
  return {}
36
 
37
+ def save_metadata(metadata):
38
+ """Save eBook metadata to JSON file"""
39
+ try:
40
+ with open(METADATA_FILE, 'w', encoding='utf-8') as f:
41
+ json.dump(metadata, f, indent=2, ensure_ascii=False)
42
+ except Exception as e:
43
+ print(f"Error saving metadata: {e}")
44
 
45
  def allowed_file(filename):
46
+ """Check if file extension is allowed"""
47
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
48
 
49
+ def get_file_hash(file):
50
+ """Generate unique hash for file"""
51
+ hash_md5 = hashlib.md5()
52
+ for chunk in iter(lambda: file.read(4096), b""):
53
+ hash_md5.update(chunk)
54
+ file.seek(0) # Reset file pointer
55
+ return hash_md5.hexdigest()
56
+
57
+ # Default cover images for eBooks
58
+ DEFAULT_COVERS = [
59
+ "https://images.unsplash.com/photo-1512820790803-83ca734da794?w=500",
60
+ "https://images.unsplash.com/photo-1495446815901-a7297e633e8d?w=500",
61
+ "https://images.unsplash.com/photo-1532012197267-da84d127e765?w=500",
62
+ "https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?w=500",
63
+ "https://images.unsplash.com/photo-1524995997946-a1c2e315a42f?w=500"
64
+ ]
65
+
66
+ # HTML template for testing (optional)
67
+ INDEX_TEMPLATE = '''
68
+ <!DOCTYPE html>
69
+ <html lang="en">
70
+ <head>
71
+ <meta charset="UTF-8">
72
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
73
+ <title>📚 eBook Server - Hugging Face</title>
74
+ <style>
75
+ * {
76
+ margin: 0;
77
+ padding: 0;
78
+ box-sizing: border-box;
79
+ }
80
+
81
+ body {
82
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
83
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
84
+ min-height: 100vh;
85
+ padding: 20px;
86
+ }
87
+
88
+ .container {
89
+ max-width: 1200px;
90
+ margin: 0 auto;
91
+ background: rgba(255, 255, 255, 0.95);
92
+ backdrop-filter: blur(10px);
93
+ border-radius: 20px;
94
+ padding: 30px;
95
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
96
+ }
97
+
98
+ h1 {
99
+ font-size: 2.5rem;
100
+ background: linear-gradient(135deg, #667eea, #764ba2);
101
+ -webkit-background-clip: text;
102
+ -webkit-text-fill-color: transparent;
103
+ margin-bottom: 10px;
104
+ }
105
+
106
+ .subtitle {
107
+ color: #666;
108
+ margin-bottom: 30px;
109
+ }
110
+
111
+ .upload-section {
112
+ background: #f8f9fa;
113
+ border-radius: 15px;
114
+ padding: 25px;
115
+ margin-bottom: 30px;
116
+ border: 2px dashed #667eea;
117
+ }
118
+
119
+ .upload-section h3 {
120
+ color: #333;
121
+ margin-bottom: 20px;
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 10px;
125
+ }
126
+
127
+ .form-group {
128
+ margin-bottom: 15px;
129
+ }
130
 
131
+ input, select {
132
+ width: 100%;
133
+ padding: 12px 15px;
134
+ border: 1px solid #ddd;
135
+ border-radius: 10px;
136
+ font-size: 14px;
137
+ transition: border-color 0.3s;
138
+ }
139
+
140
+ input:focus, select:focus {
141
+ outline: none;
142
+ border-color: #667eea;
143
+ }
144
+
145
+ .btn {
146
+ padding: 12px 25px;
147
+ background: linear-gradient(135deg, #667eea, #764ba2);
148
+ color: white;
149
+ border: none;
150
+ border-radius: 10px;
151
+ font-size: 16px;
152
+ font-weight: 600;
153
+ cursor: pointer;
154
+ transition: transform 0.2s;
155
+ }
156
+
157
+ .btn:hover {
158
+ transform: translateY(-2px);
159
+ }
160
+
161
+ .btn-secondary {
162
+ background: linear-gradient(135deg, #10b981, #059669);
163
+ }
164
+
165
+ .btn-danger {
166
+ background: linear-gradient(135deg, #ef4444, #dc2626);
167
+ }
168
+
169
+ .ebooks-grid {
170
+ display: grid;
171
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
172
+ gap: 20px;
173
+ margin-top: 20px;
174
+ }
175
+
176
+ .ebook-card {
177
+ background: white;
178
+ border-radius: 15px;
179
+ overflow: hidden;
180
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
181
+ transition: transform 0.3s;
182
+ }
183
+
184
+ .ebook-card:hover {
185
+ transform: translateY(-5px);
186
+ }
187
+
188
+ .ebook-cover {
189
+ width: 100%;
190
+ height: 200px;
191
+ object-fit: cover;
192
+ background: #f0f0f0;
193
+ }
194
+
195
+ .ebook-info {
196
+ padding: 15px;
197
+ }
198
+
199
+ .ebook-title {
200
+ font-size: 1.1rem;
201
+ font-weight: 700;
202
+ margin-bottom: 5px;
203
+ color: #333;
204
+ }
205
+
206
+ .ebook-meta {
207
+ font-size: 0.85rem;
208
+ color: #666;
209
+ margin-bottom: 10px;
210
+ }
211
+
212
+ .ebook-badge {
213
+ display: inline-block;
214
+ padding: 4px 10px;
215
+ background: linear-gradient(135deg, #f59e0b, #d97706);
216
+ color: white;
217
+ border-radius: 20px;
218
+ font-size: 0.7rem;
219
+ font-weight: 700;
220
+ margin-bottom: 10px;
221
+ }
222
+
223
+ .ebook-actions {
224
+ display: flex;
225
+ gap: 10px;
226
+ margin-top: 10px;
227
+ }
228
+
229
+ .ebook-actions button {
230
+ flex: 1;
231
+ padding: 8px;
232
+ border: none;
233
+ border-radius: 8px;
234
+ cursor: pointer;
235
+ font-size: 0.85rem;
236
+ font-weight: 600;
237
+ transition: opacity 0.2s;
238
+ }
239
+
240
+ .stats {
241
+ display: flex;
242
+ gap: 20px;
243
+ margin: 20px 0;
244
+ padding: 20px;
245
+ background: linear-gradient(135deg, #667eea10, #764ba210);
246
+ border-radius: 15px;
247
+ }
248
+
249
+ .stat-item {
250
+ flex: 1;
251
+ text-align: center;
252
+ }
253
+
254
+ .stat-value {
255
+ font-size: 2rem;
256
+ font-weight: 700;
257
+ color: #667eea;
258
+ }
259
+
260
+ .stat-label {
261
+ color: #666;
262
+ font-size: 0.9rem;
263
+ }
264
+
265
+ .search-bar {
266
+ display: flex;
267
+ gap: 10px;
268
+ margin-bottom: 20px;
269
+ }
270
+
271
+ .search-bar input {
272
+ flex: 1;
273
+ }
274
+
275
+ .toast {
276
+ position: fixed;
277
+ top: 20px;
278
+ right: 20px;
279
+ padding: 15px 25px;
280
+ background: #10b981;
281
+ color: white;
282
+ border-radius: 10px;
283
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
284
+ animation: slideIn 0.3s ease;
285
+ z-index: 1000;
286
+ }
287
+
288
+ .toast.error {
289
+ background: #ef4444;
290
+ }
291
+
292
+ @keyframes slideIn {
293
+ from { transform: translateX(100%); opacity: 0; }
294
+ to { transform: translateX(0); opacity: 1; }
295
+ }
296
+
297
+ .loading {
298
+ text-align: center;
299
+ padding: 40px;
300
+ color: #666;
301
+ }
302
+
303
+ .loading::after {
304
+ content: '';
305
+ display: inline-block;
306
+ width: 20px;
307
+ height: 20px;
308
+ border: 3px solid #f3f3f3;
309
+ border-top: 3px solid #667eea;
310
+ border-radius: 50%;
311
+ animation: spin 1s linear infinite;
312
+ margin-left: 10px;
313
+ }
314
+
315
+ @keyframes spin {
316
+ 0% { transform: rotate(0deg); }
317
+ 100% { transform: rotate(360deg); }
318
+ }
319
+ </style>
320
+ </head>
321
+ <body>
322
+ <div class="container">
323
+ <h1>📚 eBook Management Server</h1>
324
+ <div class="subtitle">Hugging Face Backend for Quotes Platform</div>
325
+
326
+ <div class="stats" id="stats">
327
+ <div class="stat-item">
328
+ <div class="stat-value" id="totalEbooks">0</div>
329
+ <div class="stat-label">Total eBooks</div>
330
+ </div>
331
+ <div class="stat-item">
332
+ <div class="stat-value" id="totalDownloads">0</div>
333
+ <div class="stat-label">Total Downloads</div>
334
+ </div>
335
+ <div class="stat-item">
336
+ <div class="stat-value" id="storageUsed">0 MB</div>
337
+ <div class="stat-label">Storage Used</div>
338
+ </div>
339
+ </div>
340
+
341
+ <div class="upload-section">
342
+ <h3><span>📤</span> Upload New eBook</h3>
343
+ <form id="uploadForm" enctype="multipart/form-data">
344
+ <div class="form-group">
345
+ <input type="file" name="file" accept=".pdf,.epub,.txt,.docx" required>
346
+ </div>
347
+ <div class="form-group">
348
+ <input type="text" name="title" placeholder="Book Title (optional)">
349
+ </div>
350
+ <div class="form-group">
351
+ <input type="text" name="author" placeholder="Author (optional)">
352
+ </div>
353
+ <div class="form-group">
354
+ <select name="category">
355
+ <option value="General">General</option>
356
+ <option value="Fiction">Fiction</option>
357
+ <option value="Non-Fiction">Non-Fiction</option>
358
+ <option value="Poetry">Poetry</option>
359
+ <option value="Philosophy">Philosophy</option>
360
+ <option value="Self-Help">Self-Help</option>
361
+ <option value="Science">Science</option>
362
+ <option value="Technology">Technology</option>
363
+ </select>
364
+ </div>
365
+ <div class="form-group">
366
+ <input type="url" name="image_url" placeholder="Cover Image URL (optional)">
367
+ </div>
368
+ <div class="form-group">
369
+ <input type="text" name="uploaded_by" placeholder="Your User ID" required>
370
+ </div>
371
+ <button type="submit" class="btn">📤 Upload eBook</button>
372
+ </form>
373
+ </div>
374
+
375
+ <div class="search-bar">
376
+ <input type="text" id="searchInput" placeholder="Search eBooks by title, author, or category...">
377
+ <button class="btn btn-secondary" onclick="searchEbooks()">🔍 Search</button>
378
+ <button class="btn" onclick="fetchEbooks()">⟲ Refresh</button>
379
+ </div>
380
+
381
+ <div class="ebooks-grid" id="ebooksGrid">
382
+ <div class="loading">Loading eBooks...</div>
383
+ </div>
384
+ </div>
385
+
386
+ <script>
387
+ // Show toast notification
388
+ function showToast(message, isError = false) {
389
+ const toast = document.createElement('div');
390
+ toast.className = `toast ${isError ? 'error' : ''}`;
391
+ toast.textContent = message;
392
+ document.body.appendChild(toast);
393
+
394
+ setTimeout(() => {
395
+ toast.style.opacity = '0';
396
+ setTimeout(() => toast.remove(), 300);
397
+ }, 3000);
398
+ }
399
+
400
+ // Load eBooks on page load
401
+ window.onload = function() {
402
+ fetchEbooks();
403
+ };
404
+
405
+ document.getElementById('uploadForm').onsubmit = async function(e) {
406
+ e.preventDefault();
407
+ const formData = new FormData(this);
408
+
409
+ try {
410
+ const response = await fetch('/api/upload', {
411
+ method: 'POST',
412
+ body: formData
413
+ });
414
+ const data = await response.json();
415
+
416
+ if (response.ok) {
417
+ showToast('✅ eBook uploaded successfully!');
418
+ this.reset();
419
+ fetchEbooks();
420
+ } else {
421
+ showToast('❌ Upload failed: ' + data.error, true);
422
+ }
423
+ } catch (error) {
424
+ showToast('❌ Error uploading file', true);
425
+ }
426
+ };
427
+
428
+ async function fetchEbooks() {
429
+ document.getElementById('ebooksGrid').innerHTML = '<div class="loading">Loading eBooks...</div>';
430
+
431
+ try {
432
+ const response = await fetch('/api/ebooks');
433
+ const ebooks = await response.json();
434
+ displayEbooks(ebooks);
435
+ updateStats(ebooks);
436
+ } catch (error) {
437
+ document.getElementById('ebooksGrid').innerHTML = '<div style="text-align:center; padding:40px; color:#666;">❌ Error loading eBooks</div>';
438
+ }
439
+ }
440
+
441
+ async function searchEbooks() {
442
+ const query = document.getElementById('searchInput').value;
443
+ if (!query) {
444
+ fetchEbooks();
445
+ return;
446
+ }
447
+
448
+ document.getElementById('ebooksGrid').innerHTML = '<div class="loading">Searching...</div>';
449
+
450
+ try {
451
+ const response = await fetch(`/api/ebooks/search?q=${encodeURIComponent(query)}`);
452
+ const ebooks = await response.json();
453
+ displayEbooks(ebooks);
454
+ } catch (error) {
455
+ document.getElementById('ebooksGrid').innerHTML = '<div style="text-align:center; padding:40px; color:#666;">❌ Error searching eBooks</div>';
456
+ }
457
+ }
458
+
459
+ function updateStats(ebooks) {
460
+ document.getElementById('totalEbooks').textContent = ebooks.length;
461
+
462
+ const totalDownloads = ebooks.reduce((sum, ebook) => sum + (ebook.download_count || 0), 0);
463
+ document.getElementById('totalDownloads').textContent = totalDownloads;
464
+
465
+ const totalSizeMB = (ebooks.reduce((sum, ebook) => sum + (ebook.size || 0), 0) / (1024 * 1024)).toFixed(2);
466
+ document.getElementById('storageUsed').textContent = totalSizeMB + ' MB';
467
+ }
468
+
469
+ function displayEbooks(ebooks) {
470
+ const grid = document.getElementById('ebooksGrid');
471
+
472
+ if (!ebooks || ebooks.length === 0) {
473
+ grid.innerHTML = '<div style="text-align:center; padding:40px; color:#666;">📭 No eBooks available</div>';
474
+ return;
475
+ }
476
+
477
+ grid.innerHTML = ebooks.map(ebook => {
478
+ const coverImage = ebook.image_url || 'https://images.unsplash.com/photo-1512820790803-83ca734da794?w=500';
479
+ const uploadDate = new Date(ebook.upload_date).toLocaleDateString();
480
+ const sizeKB = (ebook.size / 1024).toFixed(2);
481
+
482
+ return `
483
+ <div class="ebook-card">
484
+ <img src="${coverImage}"
485
+ class="ebook-cover"
486
+ onerror="this.src='https://images.unsplash.com/photo-1512820790803-83ca734da794?w=500'">
487
+ <div class="ebook-info">
488
+ <div class="ebook-title">${ebook.title || ebook.filename}</div>
489
+ <div class="ebook-meta">
490
+ By ${ebook.author || 'Unknown'} • ${ebook.category || 'General'}<br>
491
+ Uploaded: ${uploadDate}<br>
492
+ Size: ${sizeKB} KB • Downloads: ${ebook.download_count || 0}
493
+ </div>
494
+ <span class="ebook-badge">FREE</span>
495
+ <div class="ebook-actions">
496
+ <button style="background: linear-gradient(135deg, #10b981, #059669); color: white;"
497
+ onclick="downloadEbook('${ebook.id}')">
498
+ 📥 Download
499
+ </button>
500
+ <button style="background: linear-gradient(135deg, #ef4444, #dc2626); color: white;"
501
+ onclick="promptDelete('${ebook.id}', '${ebook.uploaded_by}')">
502
+ 🗑️ Delete
503
+ </button>
504
+ </div>
505
+ </div>
506
+ </div>
507
+ `;
508
+ }).join('');
509
+ }
510
+
511
+ async function downloadEbook(ebookId) {
512
+ window.location.href = `/api/download/${ebookId}`;
513
+ }
514
+
515
+ function promptDelete(ebookId, uploadedBy) {
516
+ const userId = prompt('Enter your User ID to delete this eBook:');
517
+ if (!userId) return;
518
+
519
+ if (userId !== uploadedBy) {
520
+ showToast('❌ Only the uploader can delete this eBook', true);
521
+ return;
522
+ }
523
+
524
+ if (confirm('Are you sure you want to delete this eBook?')) {
525
+ deleteEbook(ebookId, userId);
526
+ }
527
+ }
528
+
529
+ async function deleteEbook(ebookId, userId) {
530
+ try {
531
+ const response = await fetch(`/api/ebooks/${ebookId}`, {
532
+ method: 'DELETE',
533
+ headers: { 'Content-Type': 'application/json' },
534
+ body: JSON.stringify({ user_id: userId })
535
+ });
536
+
537
+ const data = await response.json();
538
+
539
+ if (response.ok) {
540
+ showToast('✅ eBook deleted successfully');
541
+ fetchEbooks();
542
+ } else {
543
+ showToast('❌ Delete failed: ' + data.error, true);
544
+ }
545
+ } catch (error) {
546
+ showToast('❌ Error deleting eBook', true);
547
+ }
548
+ }
549
+
550
+ // Search on enter key
551
+ document.getElementById('searchInput').addEventListener('keypress', function(e) {
552
+ if (e.key === 'Enter') {
553
+ searchEbooks();
554
+ }
555
+ });
556
+ </script>
557
+ </body>
558
+ </html>
559
+ '''
560
+
561
+ @app.route('/')
562
+ def index():
563
+ """Test interface (optional)"""
564
+ return render_template_string(INDEX_TEMPLATE)
565
 
566
  @app.route('/api/upload', methods=['POST'])
567
  def upload_ebook():
568
+ """Handle eBook upload"""
569
  try:
570
+ # Check if file exists in request
571
  if 'file' not in request.files:
572
  return jsonify({'error': 'No file provided'}), 400
573
+
574
  file = request.files['file']
575
+
576
  if file.filename == '':
577
  return jsonify({'error': 'No file selected'}), 400
578
+
579
  if not allowed_file(file.filename):
580
+ return jsonify({'error': f'File type not allowed. Allowed types: {", ".join(ALLOWED_EXTENSIONS)}'}), 400
581
+
582
+ # Get additional metadata
583
  title = request.form.get('title', '')
584
  author = request.form.get('author', '')
585
  category = request.form.get('category', 'General')
586
  uploaded_by = request.form.get('uploaded_by', '')
587
  image_url = request.form.get('image_url', '')
588
+
 
589
  if not uploaded_by:
590
  return jsonify({'error': 'User ID required'}), 400
591
+
592
+ # Generate unique filename
593
  original_filename = secure_filename(file.filename)
594
  file_ext = original_filename.rsplit('.', 1)[1].lower()
595
  unique_id = str(uuid.uuid4())
596
+ filename = f"{unique_id}_{original_filename}"
597
+ filepath = os.path.join(UPLOAD_FOLDER, filename)
598
+
599
+ # Save file
600
  file.save(filepath)
 
601
  file_size = os.path.getsize(filepath)
602
+
603
+ # Check file size
604
  if file_size > MAX_FILE_SIZE:
605
  os.remove(filepath)
606
+ return jsonify({'error': f'File too large. Max size: {MAX_FILE_SIZE/1024/1024}MB'}), 400
607
+
608
+ # Select random default cover if no image URL provided
609
+ if not image_url:
610
+ import random
611
+ image_url = random.choice(DEFAULT_COVERS)
612
+
613
+ # Create metadata
614
+ metadata = {
615
+ 'id': unique_id,
616
+ 'filename': original_filename,
617
+ 'stored_filename': filename,
618
+ 'title': title or original_filename,
619
+ 'author': author or 'Unknown',
620
+ 'category': category,
621
+ 'image_url': image_url,
622
+ 'uploaded_by': uploaded_by,
623
+ 'size': file_size,
624
+ 'upload_date': datetime.datetime.now().isoformat(),
625
+ 'download_count': 0
626
+ }
627
+
628
+ # Save metadata
629
  ebooks_metadata = load_metadata()
630
+ ebooks_metadata[unique_id] = metadata
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  save_metadata(ebooks_metadata)
632
+
633
  return jsonify({
634
+ 'message': 'eBook uploaded successfully',
635
+ 'ebook': metadata
636
  }), 201
637
+
638
  except Exception as e:
639
+ return jsonify({'error': str(e)}), 500
 
 
 
640
 
641
  @app.route('/api/ebooks', methods=['GET'])
642
  def get_ebooks():
643
+ """Get list of all eBooks"""
644
  try:
645
  ebooks_metadata = load_metadata()
646
+ # Remove sensitive information and add download URL
647
+ ebook_list = []
648
+ for ebook_id, metadata in ebooks_metadata.items():
649
+ ebook_info = metadata.copy()
650
+ ebook_info['download_url'] = f'/api/download/{ebook_id}'
651
+ ebook_list.append(ebook_info)
652
+
653
+ # Sort by upload date (newest first)
654
+ ebook_list.sort(key=lambda x: x.get('upload_date', ''), reverse=True)
655
+
656
  return jsonify(ebook_list), 200
657
+
658
  except Exception as e:
659
+ return jsonify({'error': str(e)}), 500
 
 
 
660
 
661
  @app.route('/api/download/<ebook_id>', methods=['GET'])
662
  def download_ebook(ebook_id):
663
+ """Download specific eBook"""
664
  try:
665
  ebooks_metadata = load_metadata()
666
+
667
  if ebook_id not in ebooks_metadata:
668
+ return jsonify({'error': 'eBook not found'}), 404
669
+
670
+ metadata = ebooks_metadata[ebook_id]
671
+ filepath = os.path.join(UPLOAD_FOLDER, metadata['stored_filename'])
672
+
673
  if not os.path.exists(filepath):
674
+ return jsonify({'error': 'File not found on server'}), 404
675
+
676
  # Increment download count
677
+ metadata['download_count'] = metadata.get('download_count', 0) + 1
678
  save_metadata(ebooks_metadata)
679
+
 
 
680
  return send_file(
681
  filepath,
682
  as_attachment=True,
683
+ download_name=metadata['filename'],
684
+ mimetype='application/octet-stream'
685
  )
686
+
687
  except Exception as e:
688
+ return jsonify({'error': str(e)}), 500
 
 
 
689
 
690
  @app.route('/api/ebooks/<ebook_id>', methods=['DELETE'])
691
  def delete_ebook(ebook_id):
692
+ """Delete an eBook (only by uploader)"""
693
  try:
694
  data = request.get_json()
695
  user_id = data.get('user_id')
696
+
697
  if not user_id:
698
  return jsonify({'error': 'User ID required'}), 400
699
+
700
  ebooks_metadata = load_metadata()
701
+
702
  if ebook_id not in ebooks_metadata:
703
+ return jsonify({'error': 'eBook not found'}), 404
704
+
705
+ metadata = ebooks_metadata[ebook_id]
706
+
707
+ # Check if user is the uploader
708
+ if metadata['uploaded_by'] != user_id:
709
+ return jsonify({'error': 'Unauthorized: Only uploader can delete'}), 403
710
+
711
+ # Delete file
712
+ filepath = os.path.join(UPLOAD_FOLDER, metadata['stored_filename'])
713
  if os.path.exists(filepath):
714
  os.remove(filepath)
715
+
716
+ # Delete metadata
717
  del ebooks_metadata[ebook_id]
718
  save_metadata(ebooks_metadata)
719
+
720
+ return jsonify({'message': 'eBook deleted successfully'}), 200
721
+
722
  except Exception as e:
723
+ return jsonify({'error': str(e)}), 500
 
 
 
724
 
725
  @app.route('/api/ebooks/search', methods=['GET'])
726
  def search_ebooks():
727
+ """Search eBooks by title, author, or category"""
728
  try:
729
  query = request.args.get('q', '').lower()
730
+ category = request.args.get('category', '')
731
+
732
+ if not query and not category:
733
+ return get_ebooks()
734
+
735
  ebooks_metadata = load_metadata()
736
  results = []
737
+
738
+ for ebook_id, metadata in ebooks_metadata.items():
739
  match = True
740
+
741
  if query:
742
  match = match and (
743
+ query in metadata.get('title', '').lower() or
744
+ query in metadata.get('author', '').lower() or
745
+ query in metadata.get('filename', '').lower()
746
  )
747
+
748
  if category:
749
+ match = match and category.lower() == metadata.get('category', '').lower()
750
+
751
  if match:
752
+ ebook_info = metadata.copy()
753
+ ebook_info['download_url'] = f'/api/download/{ebook_id}'
754
+ results.append(ebook_info)
755
+
756
  return jsonify(results), 200
757
+
758
  except Exception as e:
759
+ return jsonify({'error': str(e)}), 500
 
 
 
760
 
761
  @app.route('/api/ebooks/user/<user_id>', methods=['GET'])
762
  def get_user_ebooks(user_id):
763
+ """Get eBooks uploaded by specific user"""
764
  try:
765
  ebooks_metadata = load_metadata()
766
+ user_ebooks = []
767
+
768
+ for ebook_id, metadata in ebooks_metadata.items():
769
+ if metadata.get('uploaded_by') == user_id:
770
+ ebook_info = metadata.copy()
771
+ ebook_info['download_url'] = f'/api/download/{ebook_id}'
772
+ user_ebooks.append(ebook_info)
773
+
774
  return jsonify(user_ebooks), 200
775
+
776
  except Exception as e:
777
+ return jsonify({'error': str(e)}), 500
778
 
779
+ # Health check endpoint
780
+ @app.route('/api/health', methods=['GET'])
781
+ def health_check():
782
+ """Health check endpoint for monitoring"""
783
+ try:
784
+ ebooks_metadata = load_metadata()
785
+ total_size = sum(m.get('size', 0) for m in ebooks_metadata.values())
786
+
787
+ return jsonify({
788
+ 'status': 'healthy',
789
+ 'timestamp': datetime.datetime.now().isoformat(),
790
+ 'stats': {
791
+ 'total_ebooks': len(ebooks_metadata),
792
+ 'total_size_mb': round(total_size / (1024 * 1024), 2),
793
+ 'allowed_extensions': list(ALLOWED_EXTENSIONS),
794
+ 'max_file_size_mb': MAX_FILE_SIZE / (1024 * 1024)
795
+ }
796
+ }), 200
797
+ except Exception as e:
798
+ return jsonify({'status': 'unhealthy', 'error': str(e)}), 500
799
 
800
+ # Error handlers
801
+ @app.errorhandler(404)
802
+ def not_found(error):
803
+ return jsonify({'error': 'Endpoint not found'}), 404
804
 
805
+ @app.errorhandler(500)
806
+ def internal_error(error):
807
+ return jsonify({'error': 'Internal server error'}), 500
 
 
 
808
 
809
+ # Clean up old files (optional - can be called periodically)
810
+ @app.route('/api/cleanup', methods=['POST'])
811
+ def cleanup_files():
812
+ """Remove orphaned files (files without metadata)"""
813
+ try:
814
+ ebooks_metadata = load_metadata()
815
+ stored_files = set(m['stored_filename'] for m in ebooks_metadata.values())
816
+
817
+ cleaned = 0
818
+ for filename in os.listdir(UPLOAD_FOLDER):
819
+ if filename not in stored_files:
820
+ filepath = os.path.join(UPLOAD_FOLDER, filename)
821
+ os.remove(filepath)
822
+ cleaned += 1
823
+
824
+ return jsonify({
825
+ 'message': f'Cleaned up {cleaned} orphaned files',
826
+ 'cleaned_count': cleaned
827
+ }), 200
828
+ except Exception as e:
829
+ return jsonify({'error': str(e)}), 500
830
 
831
  if __name__ == '__main__':
832
+ # For Hugging Face Spaces, use port 7860
833
+ port = int(os.environ.get('PORT', 7860))
834
+ app.run(host='0.0.0.0', port=port, debug=False)