albumup commited on
Commit
1982de9
·
verified ·
1 Parent(s): c9d20aa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +380 -49
app.py CHANGED
@@ -10,6 +10,7 @@ import uuid
10
  import json
11
  from datetime import datetime
12
  import httpx
 
13
  from functools import lru_cache
14
 
15
  class Settings(BaseSettings):
@@ -30,34 +31,8 @@ class Album(BaseModel):
30
 
31
  app = FastAPI()
32
 
33
- # In-memory cache for faster access
34
  albums_cache = {}
35
 
36
- async def sync_with_database():
37
- settings = get_settings()
38
- try:
39
- async with httpx.AsyncClient() as client:
40
- response = await client.get(f"{settings.database_url}/albums")
41
- if response.status_code == 200:
42
- data = response.json()
43
- albums_cache.clear()
44
- albums_cache.update(data)
45
- except Exception as e:
46
- print(f"Database sync error: {e}")
47
-
48
- async def save_to_database(album_id: str, album_data: dict):
49
- settings = get_settings()
50
- try:
51
- async with httpx.AsyncClient() as client:
52
- response = await client.post(
53
- f"{settings.database_url}/albums",
54
- json={album_id: album_data}
55
- )
56
- if response.status_code != 200:
57
- raise HTTPException(status_code=500, detail="Failed to save to database")
58
- except Exception as e:
59
- raise HTTPException(status_code=500, detail=str(e))
60
-
61
  HTML_CONTENT = """
62
  <!DOCTYPE html>
63
  <html lang="en">
@@ -77,28 +52,50 @@ HTML_CONTENT = """
77
  border: 1px solid #ddd;
78
  border-radius: 5px;
79
  }
 
 
 
80
  .search-form {
81
  margin: 20px 0;
82
  padding: 20px;
83
  border: 1px solid #ddd;
84
  border-radius: 5px;
85
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  </style>
87
  </head>
88
  <body>
89
  <div class="container">
90
  <h1>File Upload & Album Management</h1>
91
 
92
- <!-- Search Albums -->
93
  <div class="search-form">
94
  <h2>Search Albums</h2>
95
  <form action="/search" method="get">
96
  <input type="text" name="query" placeholder="Search by album name..." required>
97
- <button type="submit">Search</button>
98
  </form>
99
  </div>
100
 
101
- <!-- Album Upload -->
102
  <div class="upload-form">
103
  <h2>Create Album</h2>
104
  <form action="/album/create" method="post" enctype="multipart/form-data">
@@ -106,13 +103,271 @@ HTML_CONTENT = """
106
  <input type="checkbox" name="is_private" id="is_private">
107
  <label for="is_private">Make Album Private</label><br><br>
108
  <input type="file" name="files" accept="*/*" multiple required><br><br>
109
- <button type="submit">Create Album</button>
110
  </form>
111
  </div>
112
  </div>
113
  </body>
114
  </html>
115
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  @app.on_event("startup")
118
  async def startup_event():
@@ -122,15 +377,35 @@ async def startup_event():
122
  async def index():
123
  return HTML_CONTENT
124
 
125
- @app.get("/search")
126
- async def search_albums(query: str = "", include_private: bool = False):
 
127
  matching_albums = {
128
  album_id: album for album_id, album in albums_cache.items()
129
- if (query.lower() in album['name'].lower() and
130
- (not album['is_private'] or include_private))
131
  }
132
- return {"albums": matching_albums}
133
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  @app.post("/album/create")
135
  async def create_album(
136
  request: Request,
@@ -163,7 +438,7 @@ async def create_album(
163
  'created_at': datetime.now().isoformat(),
164
  'is_private': is_private,
165
  'files': album_files,
166
- 'owner': 'anonymous' # You can implement user authentication later
167
  }
168
 
169
  albums_cache[album_id] = album_data
@@ -172,17 +447,60 @@ async def create_album(
172
  base_url = str(request.base_url).rstrip('/')
173
  return RedirectResponse(url=f"{base_url}/album/{album_id}", status_code=303)
174
 
175
- @app.get("/album/{album_id}")
176
- async def view_album(album_id: str, request: Request):
177
  if album_id not in albums_cache:
178
- raise HTTPException(status_code=404, detail="Album not found")
179
 
180
  album = albums_cache[album_id]
181
- if album['is_private']:
182
- # Implement authentication check here
183
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- return album
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
  @app.get("/album/{album_id}/download")
188
  async def download_album(album_id: str):
@@ -190,10 +508,7 @@ async def download_album(album_id: str):
190
  raise HTTPException(status_code=404, detail="Album not found")
191
 
192
  album = albums_cache[album_id]
193
- if album['is_private']:
194
- # Implement authentication check here
195
- pass
196
-
197
  import io
198
  import zipfile
199
 
@@ -213,7 +528,23 @@ async def download_album(album_id: str):
213
  }
214
  )
215
 
216
- # Helper functions remain the same as in previous version
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  async def get_cookies():
218
  try:
219
  response = requests.get('https://replicate.com/levelsio/neon-tokyo', headers={
 
10
  import json
11
  from datetime import datetime
12
  import httpx
13
+ import mimetypes
14
  from functools import lru_cache
15
 
16
  class Settings(BaseSettings):
 
31
 
32
  app = FastAPI()
33
 
 
34
  albums_cache = {}
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  HTML_CONTENT = """
37
  <!DOCTYPE html>
38
  <html lang="en">
 
52
  border: 1px solid #ddd;
53
  border-radius: 5px;
54
  }
55
+ .preview {
56
+ margin-top: 20px;
57
+ }
58
  .search-form {
59
  margin: 20px 0;
60
  padding: 20px;
61
  border: 1px solid #ddd;
62
  border-radius: 5px;
63
  }
64
+ .album-list {
65
+ margin-top: 20px;
66
+ }
67
+ .album-item {
68
+ padding: 10px;
69
+ border: 1px solid #ddd;
70
+ margin-bottom: 10px;
71
+ border-radius: 5px;
72
+ }
73
+ .button {
74
+ display: inline-block;
75
+ padding: 8px 16px;
76
+ background-color: #007bff;
77
+ color: white;
78
+ text-decoration: none;
79
+ border-radius: 4px;
80
+ margin: 5px;
81
+ }
82
+ .button:hover {
83
+ background-color: #0056b3;
84
+ }
85
  </style>
86
  </head>
87
  <body>
88
  <div class="container">
89
  <h1>File Upload & Album Management</h1>
90
 
 
91
  <div class="search-form">
92
  <h2>Search Albums</h2>
93
  <form action="/search" method="get">
94
  <input type="text" name="query" placeholder="Search by album name..." required>
95
+ <button type="submit" class="button">Search</button>
96
  </form>
97
  </div>
98
 
 
99
  <div class="upload-form">
100
  <h2>Create Album</h2>
101
  <form action="/album/create" method="post" enctype="multipart/form-data">
 
103
  <input type="checkbox" name="is_private" id="is_private">
104
  <label for="is_private">Make Album Private</label><br><br>
105
  <input type="file" name="files" accept="*/*" multiple required><br><br>
106
+ <button type="submit" class="button">Create Album</button>
107
  </form>
108
  </div>
109
  </div>
110
  </body>
111
  </html>
112
  """
113
+ ALBUM_VIEW_HTML = """
114
+ <!DOCTYPE html>
115
+ <html lang="en">
116
+ <head>
117
+ <meta charset="UTF-8">
118
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
119
+ <title>Album View - {album_name}</title>
120
+ <style>
121
+ .container {{
122
+ max-width: 800px;
123
+ margin: 0 auto;
124
+ padding: 20px;
125
+ }}
126
+ .file-grid {{
127
+ display: grid;
128
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
129
+ gap: 20px;
130
+ }}
131
+ .file-item {{
132
+ border: 1px solid #ddd;
133
+ padding: 10px;
134
+ text-align: center;
135
+ border-radius: 5px;
136
+ }}
137
+ .download-all {{
138
+ margin: 20px 0;
139
+ }}
140
+ .back-link {{
141
+ margin-bottom: 20px;
142
+ display: block;
143
+ }}
144
+ .button {{
145
+ display: inline-block;
146
+ padding: 8px 16px;
147
+ background-color: #007bff;
148
+ color: white;
149
+ text-decoration: none;
150
+ border-radius: 4px;
151
+ margin: 5px;
152
+ }}
153
+ .button:hover {{
154
+ background-color: #0056b3;
155
+ }}
156
+ .preview-container {{
157
+ margin-bottom: 10px;
158
+ max-width: 100%;
159
+ height: 200px;
160
+ overflow: hidden;
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ background-color: #f8f9fa;
165
+ border-radius: 4px;
166
+ }}
167
+ .preview-container img {{
168
+ max-width: 100%;
169
+ max-height: 100%;
170
+ object-fit: contain;
171
+ }}
172
+ .preview-container video {{
173
+ max-width: 100%;
174
+ max-height: 100%;
175
+ object-fit: contain;
176
+ }}
177
+ .modal {{
178
+ display: none;
179
+ position: fixed;
180
+ z-index: 1000;
181
+ top: 0;
182
+ left: 0;
183
+ width: 100%;
184
+ height: 100%;
185
+ background-color: rgba(0,0,0,0.9);
186
+ overflow: auto;
187
+ }}
188
+ .modal-content {{
189
+ margin: auto;
190
+ display: block;
191
+ max-width: 90%;
192
+ max-height: 90vh;
193
+ margin-top: 50px;
194
+ }}
195
+ .close {{
196
+ position: absolute;
197
+ right: 35px;
198
+ top: 15px;
199
+ color: #f1f1f1;
200
+ font-size: 40px;
201
+ font-weight: bold;
202
+ cursor: pointer;
203
+ }}
204
+ .privacy-badge {{
205
+ display: inline-block;
206
+ padding: 5px 10px;
207
+ border-radius: 4px;
208
+ margin: 10px 0;
209
+ color: white;
210
+ }}
211
+ .private {{
212
+ background-color: #dc3545;
213
+ }}
214
+ .public {{
215
+ background-color: #28a745;
216
+ }}
217
+ </style>
218
+ <script>
219
+ function openModal(url) {{
220
+ const modal = document.getElementById('previewModal');
221
+ const modalContent = document.getElementById('modalContent');
222
+ const contentType = getContentType(url);
223
+
224
+ if (contentType === 'image') {{
225
+ modalContent.innerHTML = `<img src="${{url}}" style="max-width:100%;max-height:90vh;">`;
226
+ }} else if (contentType === 'video') {{
227
+ modalContent.innerHTML = `<video controls style="max-width:100%;max-height:90vh;">
228
+ <source src="${{url}}" type="video/mp4">
229
+ Your browser does not support the video tag.
230
+ </video>`;
231
+ }}
232
+
233
+ modal.style.display = 'block';
234
+ }}
235
+
236
+ function closeModal() {{
237
+ const modal = document.getElementById('previewModal');
238
+ modal.style.display = 'none';
239
+ const modalContent = document.getElementById('modalContent');
240
+ modalContent.innerHTML = '';
241
+ }}
242
+
243
+ function getContentType(url) {{
244
+ const ext = url.split('.').pop().toLowerCase();
245
+ if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) return 'image';
246
+ if (['mp4', 'webm', 'ogg'].includes(ext)) return 'video';
247
+ return 'other';
248
+ }}
249
+ </script>
250
+ </head>
251
+ <body>
252
+ <div class="container">
253
+ <a href="/" class="back-link button">← Back to Home</a>
254
+ <h1>{album_name}</h1>
255
+ <div class="privacy-badge {privacy_class}">{privacy_status}</div>
256
+ <div class="download-all">
257
+ <a href="/album/{album_id}/download" class="button" target="_blank">Download All Files</a>
258
+ </div>
259
+ <div class="file-grid">
260
+ {file_list}
261
+ </div>
262
+ </div>
263
+
264
+ <div id="previewModal" class="modal" onclick="closeModal()">
265
+ <span class="close">&times;</span>
266
+ <div id="modalContent" class="modal-content">
267
+ </div>
268
+ </div>
269
+ </body>
270
+ </html>
271
+ """
272
+ SEARCH_RESULTS_HTML = """
273
+ <!DOCTYPE html>
274
+ <html lang="en">
275
+ <head>
276
+ <meta charset="UTF-8">
277
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
278
+ <title>Search Results</title>
279
+ <style>
280
+ .container {{
281
+ max-width: 800px;
282
+ margin: 0 auto;
283
+ padding: 20px;
284
+ }}
285
+ .album-item {{
286
+ border: 1px solid #ddd;
287
+ padding: 15px;
288
+ margin-bottom: 15px;
289
+ border-radius: 5px;
290
+ }}
291
+ .back-link {{
292
+ margin-bottom: 20px;
293
+ display: block;
294
+ }}
295
+ .button {{
296
+ display: inline-block;
297
+ padding: 8px 16px;
298
+ background-color: #007bff;
299
+ color: white;
300
+ text-decoration: none;
301
+ border-radius: 4px;
302
+ margin: 5px;
303
+ }}
304
+ .button:hover {{
305
+ background-color: #0056b3;
306
+ }}
307
+ .privacy-badge {{
308
+ display: inline-block;
309
+ padding: 5px 10px;
310
+ border-radius: 4px;
311
+ margin-left: 10px;
312
+ color: white;
313
+ }}
314
+ .private {{
315
+ background-color: #dc3545;
316
+ }}
317
+ .public {{
318
+ background-color: #28a745;
319
+ }}
320
+ </style>
321
+ </head>
322
+ <body>
323
+ <div class="container">
324
+ <a href="/" class="back-link button">← Back to Home</a>
325
+ <h1>Search Results</h1>
326
+ <p>Found {result_count} albums for "{query}"</p>
327
+ <div class="search-results">
328
+ {results}
329
+ </div>
330
+ </div>
331
+ </body>
332
+ </html>
333
+ """
334
+
335
+ async def sync_with_database():
336
+ settings = get_settings()
337
+ try:
338
+ async with httpx.AsyncClient() as client:
339
+ response = await client.get(f"{settings.database_url}/albums")
340
+ if response.status_code == 200:
341
+ data = response.json()
342
+ albums_cache.clear()
343
+ albums_cache.update(data)
344
+ except Exception as e:
345
+ print(f"Database sync error: {e}")
346
+
347
+ async def save_to_database(album_id: str, album_data: dict):
348
+ settings = get_settings()
349
+ try:
350
+ async with httpx.AsyncClient() as client:
351
+ response = await client.post(
352
+ f"{settings.database_url}",
353
+ json={
354
+ "key": album_id,
355
+ "value": album_data
356
+ }
357
+ )
358
+ if response.status_code != 200:
359
+ raise HTTPException(status_code=500, detail="Failed to save to database")
360
+ except Exception as e:
361
+ raise HTTPException(status_code=500, detail=str(e))
362
+
363
+ def get_file_type(filename):
364
+ mime_type, _ = mimetypes.guess_type(filename)
365
+ if mime_type:
366
+ if mime_type.startswith('image/'):
367
+ return 'image'
368
+ elif mime_type.startswith('video/'):
369
+ return 'video'
370
+ return 'other'
371
 
372
  @app.on_event("startup")
373
  async def startup_event():
 
377
  async def index():
378
  return HTML_CONTENT
379
 
380
+ @app.get("/search", response_class=HTMLResponse)
381
+ async def search_albums(query: str = ""):
382
+ query = query.lower()
383
  matching_albums = {
384
  album_id: album for album_id, album in albums_cache.items()
385
+ if query in album['name'].lower() and not album.get('is_private', False)
 
386
  }
387
+
388
+ results_html = ""
389
+ for album_id, album in matching_albums.items():
390
+ privacy_status = "Private" if album.get('is_private') else "Public"
391
+ privacy_class = "private" if album.get('is_private') else "public"
392
+
393
+ results_html += f"""
394
+ <div class="album-item">
395
+ <h3>{album['name']}
396
+ <span class="privacy-badge {privacy_class}">{privacy_status}</span>
397
+ </h3>
398
+ <p>Created: {album['created_at']}</p>
399
+ <p>Files: {len(album['files'])}</p>
400
+ <a href="/album/{album_id}" class="button">View Album</a>
401
+ </div>
402
+ """
403
+
404
+ return SEARCH_RESULTS_HTML.format(
405
+ query=query,
406
+ result_count=len(matching_albums),
407
+ results=results_html
408
+ )
409
  @app.post("/album/create")
410
  async def create_album(
411
  request: Request,
 
438
  'created_at': datetime.now().isoformat(),
439
  'is_private': is_private,
440
  'files': album_files,
441
+ 'owner': 'anonymous'
442
  }
443
 
444
  albums_cache[album_id] = album_data
 
447
  base_url = str(request.base_url).rstrip('/')
448
  return RedirectResponse(url=f"{base_url}/album/{album_id}", status_code=303)
449
 
450
+ @app.get("/album/{album_id}", response_class=HTMLResponse)
451
+ async def view_album(album_id: str):
452
  if album_id not in albums_cache:
453
+ return "Album not found", 404
454
 
455
  album = albums_cache[album_id]
456
+ file_list_html = ""
457
+
458
+ for file in album['files']:
459
+ file_url = f"/upload/{file['path']}"
460
+ file_type = get_file_type(file['filename'])
461
+
462
+ preview_html = ""
463
+ if file_type == 'image':
464
+ preview_html = f"""
465
+ <div class="preview-container">
466
+ <img src="{file_url}" alt="{file['filename']}" onclick="openModal('{file_url}')">
467
+ </div>
468
+ """
469
+ elif file_type == 'video':
470
+ preview_html = f"""
471
+ <div class="preview-container">
472
+ <video onclick="openModal('{file_url}')" style="cursor: pointer;">
473
+ <source src="{file_url}" type="{file['content_type']}">
474
+ Your browser does not support the video tag.
475
+ </video>
476
+ </div>
477
+ """
478
+ else:
479
+ preview_html = f"""
480
+ <div class="preview-container">
481
+ <p>No preview available for {file['filename']}</p>
482
+ </div>
483
+ """
484
 
485
+ file_list_html += f"""
486
+ <div class="file-item">
487
+ {preview_html}
488
+ <p>{file['filename']}</p>
489
+ <a href="{file_url}" class="button" onclick="openModal('{file_url}'); return false;">View</a>
490
+ <a href="{file_url}?download=true" class="button" target="_blank">Download</a>
491
+ </div>
492
+ """
493
+
494
+ privacy_status = "Private" if album['is_private'] else "Public"
495
+ privacy_class = "private" if album['is_private'] else "public"
496
+
497
+ return ALBUM_VIEW_HTML.format(
498
+ album_name=album['name'],
499
+ album_id=album_id,
500
+ file_list=file_list_html,
501
+ privacy_status=privacy_status,
502
+ privacy_class=privacy_class
503
+ )
504
 
505
  @app.get("/album/{album_id}/download")
506
  async def download_album(album_id: str):
 
508
  raise HTTPException(status_code=404, detail="Album not found")
509
 
510
  album = albums_cache[album_id]
511
+
 
 
 
512
  import io
513
  import zipfile
514
 
 
528
  }
529
  )
530
 
531
+ @app.get("/upload/{path:path}")
532
+ async def handle_stream(path: str, request: Request):
533
+ original_url = f'https://replicate.delivery/pbxt/{path}'
534
+ range_header = request.headers.get('Range')
535
+ headers = {'Range': range_header} if range_header else {}
536
+
537
+ response = requests.get(original_url, headers=headers, stream=True)
538
+
539
+ def generate():
540
+ for chunk in response.iter_content(chunk_size=8192):
541
+ yield chunk
542
+
543
+ headers = dict(response.headers)
544
+ headers['Access-Control-Allow-Origin'] = '*'
545
+
546
+ return StreamingResponse(generate(), headers=headers, status_code=response.status_code)
547
+
548
  async def get_cookies():
549
  try:
550
  response = requests.get('https://replicate.com/levelsio/neon-tokyo', headers={