|
|
from fastapi import FastAPI, File, UploadFile, Request, Form |
|
|
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse |
|
|
from typing import List, Optional |
|
|
import requests |
|
|
import asyncio |
|
|
import uuid |
|
|
from datetime import datetime |
|
|
import mimetypes |
|
|
import httpx |
|
|
import io |
|
|
import zipfile |
|
|
import math |
|
|
|
|
|
app = FastAPI() |
|
|
|
|
|
DENO_API_URL = "https://dataerrr99.deno.dev" |
|
|
|
|
|
async def save_album_to_db(album_name: str, album_id: str, files: List[dict], is_private: bool = False): |
|
|
async with httpx.AsyncClient() as client: |
|
|
try: |
|
|
response = await client.post(DENO_API_URL, json={ |
|
|
"albumName": album_name, |
|
|
"albumLink": album_id, |
|
|
"files": files, |
|
|
"isPrivate": is_private, |
|
|
"createdAt": datetime.now().isoformat() |
|
|
}) |
|
|
return response.json() |
|
|
except Exception as e: |
|
|
print(f"Error saving to DB: {str(e)}") |
|
|
return None |
|
|
|
|
|
async def get_albums_from_db(): |
|
|
async with httpx.AsyncClient() as client: |
|
|
try: |
|
|
response = await client.get(DENO_API_URL) |
|
|
return response.json() |
|
|
except Exception as e: |
|
|
print(f"Error fetching from DB: {str(e)}") |
|
|
return {"data": []} |
|
|
|
|
|
async def get_album_from_db(album_id: str): |
|
|
async with httpx.AsyncClient() as client: |
|
|
try: |
|
|
response = await client.get(f"{DENO_API_URL}/{album_id}") |
|
|
return response.json() |
|
|
except Exception as e: |
|
|
print(f"Error fetching album from DB: {str(e)}") |
|
|
return None |
|
|
|
|
|
def get_file_type(filename): |
|
|
mime_type, _ = mimetypes.guess_type(filename) |
|
|
if mime_type: |
|
|
if mime_type.startswith('image/'): |
|
|
return 'image' |
|
|
elif mime_type.startswith('video/'): |
|
|
return 'video' |
|
|
elif mime_type.startswith('audio/'): |
|
|
return 'audio' |
|
|
return 'other' |
|
|
|
|
|
def is_allowed_file_type(filename): |
|
|
allowed_types = ['image', 'video', 'audio', 'application/zip'] |
|
|
mime_type, _ = mimetypes.guess_type(filename) |
|
|
return mime_type and any(mime_type.startswith(t) for t in allowed_types) |
|
|
|
|
|
@app.get("/", response_class=HTMLResponse) |
|
|
async def index(): |
|
|
return """ |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>File Upload</title> |
|
|
<style> |
|
|
body { |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
margin: 0; |
|
|
padding: 20px; |
|
|
background-color: #f5f5f5; |
|
|
color: #333; |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 800px; |
|
|
margin: 0 auto; |
|
|
background: white; |
|
|
padding: 30px; |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
h1 { |
|
|
font-size: 2.5em; |
|
|
margin-bottom: 20px; |
|
|
color: #007bff; |
|
|
} |
|
|
|
|
|
h2 { |
|
|
font-size: 1.8em; |
|
|
margin-top: 30px; |
|
|
margin-bottom: 15px; |
|
|
color: #333; |
|
|
} |
|
|
|
|
|
.upload-form { |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.form-group { |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.form-group label { |
|
|
display: block; |
|
|
font-weight: 500; |
|
|
margin-bottom: 8px; |
|
|
} |
|
|
|
|
|
.form-group input[type="text"], |
|
|
.form-group input[type="file"] { |
|
|
width: 100%; |
|
|
padding: 12px; |
|
|
border: 2px solid #ddd; |
|
|
border-radius: 8px; |
|
|
font-size: 1em; |
|
|
transition: border-color 0.3s ease; |
|
|
} |
|
|
|
|
|
.form-group input[type="text"]:focus, |
|
|
.form-group input[type="file"]:focus { |
|
|
border-color: #007bff; |
|
|
outline: none; |
|
|
} |
|
|
|
|
|
.file-input { |
|
|
background: #f8f9fa; |
|
|
padding: 20px; |
|
|
border-radius: 8px; |
|
|
border: 2px dashed #ddd; |
|
|
text-align: center; |
|
|
cursor: pointer; |
|
|
transition: background 0.3s ease, border-color 0.3s ease; |
|
|
} |
|
|
|
|
|
.file-input:hover { |
|
|
background: #e9ecef; |
|
|
border-color: #007bff; |
|
|
} |
|
|
|
|
|
.file-input.dragover { |
|
|
background: #e9ecef; |
|
|
border-color: #007bff; |
|
|
} |
|
|
|
|
|
.file-input input { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.file-input label { |
|
|
font-size: 1.1em; |
|
|
color: #666; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.private-checkbox { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.private-checkbox input { |
|
|
width: 18px; |
|
|
height: 18px; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.private-checkbox label { |
|
|
font-size: 1em; |
|
|
color: #666; |
|
|
} |
|
|
|
|
|
.progress-bar { |
|
|
width: 100%; |
|
|
height: 12px; |
|
|
background: #e9ecef; |
|
|
border-radius: 6px; |
|
|
margin-top: 20px; |
|
|
overflow: hidden; |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.progress-bar-inner { |
|
|
height: 100%; |
|
|
background: #007bff; |
|
|
width: 0%; |
|
|
border-radius: 6px; |
|
|
transition: width 0.3s ease; |
|
|
} |
|
|
|
|
|
.button { |
|
|
display: inline-block; |
|
|
padding: 12px 24px; |
|
|
background: #007bff; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 8px; |
|
|
font-size: 1em; |
|
|
cursor: pointer; |
|
|
transition: background 0.3s ease; |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.button:hover { |
|
|
background: #0056b3; |
|
|
} |
|
|
|
|
|
.button:disabled { |
|
|
background: #ccc; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.upload-status { |
|
|
margin-top: 20px; |
|
|
font-size: 0.9em; |
|
|
color: #666; |
|
|
} |
|
|
|
|
|
.search-form { |
|
|
margin-top: 30px; |
|
|
} |
|
|
|
|
|
.search-form input[type="text"] { |
|
|
width: calc(100% - 100px); |
|
|
padding: 12px; |
|
|
border: 2px solid #ddd; |
|
|
border-radius: 8px; |
|
|
font-size: 1em; |
|
|
transition: border-color 0.3s ease; |
|
|
} |
|
|
|
|
|
.search-form input[type="text"]:focus { |
|
|
border-color: #007bff; |
|
|
outline: none; |
|
|
} |
|
|
|
|
|
.search-form button { |
|
|
padding: 12px 24px; |
|
|
background: #28a745; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 8px; |
|
|
font-size: 1em; |
|
|
cursor: pointer; |
|
|
transition: background 0.3s ease; |
|
|
} |
|
|
|
|
|
.search-form button:hover { |
|
|
background: #218838; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.container { |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
font-size: 2em; |
|
|
} |
|
|
|
|
|
h2 { |
|
|
font-size: 1.5em; |
|
|
} |
|
|
|
|
|
.button { |
|
|
width: 100%; |
|
|
margin-top: 15px; |
|
|
} |
|
|
|
|
|
.search-form input[type="text"] { |
|
|
width: calc(100% - 90px); |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<h1>bing bong</h1> |
|
|
|
|
|
<form class="upload-form" action="/album/create" method="post" enctype="multipart/form-data" onsubmit="showProgressBar()"> |
|
|
<div class="form-group"> |
|
|
<label for="album_name">Album Name</label> |
|
|
<input type="text" id="album_name" name="album_name" placeholder="Enter album name" required> |
|
|
</div> |
|
|
|
|
|
<div class="form-group"> |
|
|
<div class="file-input" id="fileInput" ondragover="handleDragOver(event)" ondrop="handleDrop(event)"> |
|
|
<input type="file" id="files" name="files" multiple required> |
|
|
<label for="files">Drag & drop files here or click to upload</label> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="private-checkbox"> |
|
|
<input type="checkbox" id="is_private" name="is_private"> |
|
|
<label for="is_private">Make this album private (exclude from search)</label> |
|
|
</div> |
|
|
|
|
|
<div class="progress-bar" id="progressBar"> |
|
|
<div class="progress-bar-inner" id="progressBarInner"></div> |
|
|
</div> |
|
|
|
|
|
<button type="submit" class="button" id="uploadButton">Create Album</button> |
|
|
<div class="upload-status" id="uploadStatus"></div> |
|
|
</form> |
|
|
|
|
|
<h2>Search Albums</h2> |
|
|
<form class="search-form" action="/search" method="get"> |
|
|
<input type="text" name="query" placeholder="Search by album name..." required> |
|
|
<button type="submit">Search</button> |
|
|
</form> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
function showProgressBar() { |
|
|
const progressBar = document.getElementById('progressBar'); |
|
|
const progressBarInner = document.getElementById('progressBarInner'); |
|
|
const uploadButton = document.getElementById('uploadButton'); |
|
|
const uploadStatus = document.getElementById('uploadStatus'); |
|
|
|
|
|
progressBar.style.display = 'block'; |
|
|
uploadButton.disabled = true; |
|
|
uploadButton.textContent = 'Uploading...'; |
|
|
uploadStatus.textContent = 'Starting upload...'; |
|
|
|
|
|
let progress = 0; |
|
|
const interval = setInterval(() => { |
|
|
progress += 1; |
|
|
progressBarInner.style.width = progress + '%'; |
|
|
if (progress >= 90) { |
|
|
clearInterval(interval); |
|
|
} |
|
|
}, 30); |
|
|
} |
|
|
|
|
|
function handleDragOver(event) { |
|
|
event.preventDefault(); |
|
|
const fileInput = document.getElementById('fileInput'); |
|
|
fileInput.classList.add('dragover'); |
|
|
} |
|
|
|
|
|
function handleDrop(event) { |
|
|
event.preventDefault(); |
|
|
const fileInput = document.getElementById('fileInput'); |
|
|
fileInput.classList.remove('dragover'); |
|
|
const files = event.dataTransfer.files; |
|
|
document.getElementById('files').files = files; |
|
|
updateFileLabel(files); |
|
|
} |
|
|
|
|
|
function updateFileLabel(files) { |
|
|
const label = document.querySelector('.file-input label'); |
|
|
if (files.length > 0) { |
|
|
label.textContent = `${files.length} file(s) selected`; |
|
|
} else { |
|
|
label.textContent = 'Drag & drop files here or click to upload'; |
|
|
} |
|
|
} |
|
|
|
|
|
document.getElementById('files').addEventListener('change', (event) => { |
|
|
updateFileLabel(event.target.files); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
@app.post("/album/create") |
|
|
async def create_album( |
|
|
request: Request, |
|
|
album_name: str = Form(...), |
|
|
files: List[UploadFile] = File(...), |
|
|
is_private: Optional[bool] = Form(False) |
|
|
): |
|
|
for file in files: |
|
|
if not is_allowed_file_type(file.filename): |
|
|
return HTMLResponse(content=f""" |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Error</title> |
|
|
</head> |
|
|
<body> |
|
|
<h1>Error: Invalid File Type</h1> |
|
|
<p>The file '{file.filename}' is not allowed. Please upload it using the <a href="https://albumup-up1.hf.space/">Single File Upload</a>.</p> |
|
|
<a href="/" class="button">← Back to Home</a> |
|
|
</body> |
|
|
</html> |
|
|
""", status_code=400) |
|
|
|
|
|
album_id = str(uuid.uuid4()) |
|
|
album_files = [] |
|
|
|
|
|
cookies = await get_cookies() |
|
|
total_files = len(files) |
|
|
uploaded_files = 0 |
|
|
|
|
|
for file in files: |
|
|
file_content = await file.read() |
|
|
upload_result = await initiate_upload(cookies, file.filename, file.content_type) |
|
|
|
|
|
if upload_result and 'upload_url' in upload_result: |
|
|
upload_success = await retry_upload(upload_result['upload_url'], file_content, file.content_type) |
|
|
if upload_success: |
|
|
serving_path = upload_result['serving_url'].split('/pbxt/')[1] |
|
|
album_files.append({ |
|
|
'filename': file.filename, |
|
|
'path': serving_path, |
|
|
'content_type': file.content_type, |
|
|
'uploaded_at': datetime.now().isoformat() |
|
|
}) |
|
|
uploaded_files += 1 |
|
|
progress = math.floor((uploaded_files / total_files) * 100) |
|
|
print(f"Upload Progress: {progress}%") |
|
|
|
|
|
|
|
|
await save_album_to_db(album_name, album_id, album_files, is_private) |
|
|
|
|
|
base_url = str(request.base_url).rstrip('/') |
|
|
return RedirectResponse(url=f"{base_url}/album/{album_id}", status_code=303) |
|
|
|
|
|
@app.get("/album/{album_id}", response_class=HTMLResponse) |
|
|
async def view_album(album_id: str): |
|
|
album = await get_album_from_db(album_id) |
|
|
if not album or 'files' not in album: |
|
|
return "Album not found or invalid data", 404 |
|
|
|
|
|
file_list_html = "" |
|
|
|
|
|
for file in album['files']: |
|
|
file_url = f"/upload/{file['path']}" |
|
|
file_type = get_file_type(file['filename']) |
|
|
download_url = f"{file_url}?download=true" |
|
|
|
|
|
preview_html = "" |
|
|
if file_type == 'image': |
|
|
preview_html = f""" |
|
|
<div class="preview-container" onclick="openModal('{file_url}')"> |
|
|
<img src="{file_url}" alt="{file['filename']}" loading="lazy"> |
|
|
</div> |
|
|
""" |
|
|
elif file_type == 'video': |
|
|
preview_html = f""" |
|
|
<div class="preview-container" onclick="openModal('{file_url}')"> |
|
|
<video> |
|
|
<source src="{file_url}" type="{file['content_type']}"> |
|
|
</video> |
|
|
<div class="play-icon">▶</div> |
|
|
</div> |
|
|
""" |
|
|
else: |
|
|
preview_html = f""" |
|
|
<div class="preview-container"> |
|
|
<div class="file-icon"> |
|
|
<svg viewBox="0 0 24 24"> |
|
|
<path fill="currentColor" d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/> |
|
|
</svg> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
file_list_html += f""" |
|
|
<div class="file-card"> |
|
|
{preview_html} |
|
|
<div class="file-info"> |
|
|
<span class="filename">{file['filename']}</span> |
|
|
<div class="actions"> |
|
|
<a href="{file_url}" target="_blank" class="action-btn view">View</a> |
|
|
<a href="{download_url}" class="action-btn download">Download</a> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
return f""" |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>{album['albumName']}</title> |
|
|
<style> |
|
|
body {{ |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
margin: 0; |
|
|
padding: 20px; |
|
|
background-color: #f5f5f5; |
|
|
}} |
|
|
|
|
|
.album-header {{ |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 30px; |
|
|
flex-wrap: wrap; |
|
|
gap: 15px; |
|
|
}} |
|
|
|
|
|
.grid-container {{ |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
|
|
gap: 20px; |
|
|
padding: 10px; |
|
|
}} |
|
|
|
|
|
.file-card {{ |
|
|
background: white; |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
|
|
overflow: hidden; |
|
|
transition: transform 0.2s; |
|
|
}} |
|
|
|
|
|
.file-card:hover {{ |
|
|
transform: translateY(-2px); |
|
|
}} |
|
|
|
|
|
.preview-container {{ |
|
|
position: relative; |
|
|
background: #f8f9fa; |
|
|
cursor: pointer; |
|
|
aspect-ratio: 1; |
|
|
}} |
|
|
|
|
|
.preview-container img, |
|
|
.preview-container video {{ |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
object-fit: cover; |
|
|
}} |
|
|
|
|
|
.file-info {{ |
|
|
padding: 15px; |
|
|
}} |
|
|
|
|
|
.filename {{ |
|
|
display: block; |
|
|
font-size: 0.9em; |
|
|
color: #333; |
|
|
white-space: nowrap; |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
margin-bottom: 10px; |
|
|
}} |
|
|
|
|
|
.actions {{ |
|
|
display: flex; |
|
|
gap: 8px; |
|
|
}} |
|
|
|
|
|
.action-btn {{ |
|
|
flex: 1; |
|
|
padding: 8px 12px; |
|
|
text-align: center; |
|
|
border-radius: 6px; |
|
|
font-size: 0.85em; |
|
|
text-decoration: none; |
|
|
transition: all 0.2s; |
|
|
}} |
|
|
|
|
|
.view {{ |
|
|
background: #007bff; |
|
|
color: white; |
|
|
}} |
|
|
|
|
|
.view:hover {{ |
|
|
background: #0056b3; |
|
|
}} |
|
|
|
|
|
.download {{ |
|
|
background: #28a745; |
|
|
color: white; |
|
|
}} |
|
|
|
|
|
.download:hover {{ |
|
|
background: #218838; |
|
|
}} |
|
|
|
|
|
.play-icon {{ |
|
|
position: absolute; |
|
|
top: 50%; |
|
|
left: 50%; |
|
|
transform: translate(-50%, -50%); |
|
|
font-size: 2.5em; |
|
|
color: white; |
|
|
text-shadow: 0 2px 8px rgba(0,0,0,0.3); |
|
|
opacity: 0.9; |
|
|
}} |
|
|
|
|
|
.file-icon {{ |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: #666; |
|
|
}} |
|
|
|
|
|
.file-icon svg {{ |
|
|
width: 50%; |
|
|
height: 50%; |
|
|
}} |
|
|
|
|
|
/* Modal styles */ |
|
|
.modal {{ |
|
|
display: none; |
|
|
position: fixed; |
|
|
z-index: 1000; |
|
|
left: 0; |
|
|
top: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-color: rgba(0,0,0,0.9); |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
}} |
|
|
|
|
|
.modal-content {{ |
|
|
max-width: 90%; |
|
|
max-height: 90%; |
|
|
background: black; |
|
|
}} |
|
|
|
|
|
.modal-media {{ |
|
|
max-width: 100%; |
|
|
max-height: 80vh; |
|
|
object-fit: contain; |
|
|
}} |
|
|
|
|
|
.close-modal {{ |
|
|
position: absolute; |
|
|
top: 20px; |
|
|
right: 30px; |
|
|
color: white; |
|
|
font-size: 40px; |
|
|
cursor: pointer; |
|
|
z-index: 1001; |
|
|
}} |
|
|
|
|
|
@media (max-width: 768px) {{ |
|
|
.grid-container {{ |
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
|
|
}} |
|
|
|
|
|
.album-header {{ |
|
|
flex-direction: column; |
|
|
align-items: flex-start; |
|
|
}} |
|
|
}} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="album-header"> |
|
|
<h1>{album['albumName']}</h1> |
|
|
<a href="/album/{album_id}/download" class="download-all-btn">Download All as ZIP</a> |
|
|
</div> |
|
|
|
|
|
<div class="grid-container"> |
|
|
{file_list_html} |
|
|
</div> |
|
|
|
|
|
<div id="modal" class="modal" onclick="closeModal()"> |
|
|
<span class="close-modal">×</span> |
|
|
<div class="modal-content"> |
|
|
<img id="modal-media" class="modal-media" src="" alt=""> |
|
|
<video id="modal-video" class="modal-media" controls style="display: none;"></video> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
function openModal(url) {{ |
|
|
const modal = document.getElementById('modal'); |
|
|
const imgModal = document.getElementById('modal-media'); |
|
|
const videoModal = document.getElementById('modal-video'); |
|
|
|
|
|
if (url.includes('.mp4') || url.includes('.webm')) {{ |
|
|
videoModal.style.display = 'block'; |
|
|
imgModal.style.display = 'none'; |
|
|
videoModal.src = url; |
|
|
}} else {{ |
|
|
imgModal.style.display = 'block'; |
|
|
videoModal.style.display = 'none'; |
|
|
imgModal.src = url; |
|
|
}} |
|
|
|
|
|
modal.style.display = 'flex'; |
|
|
}} |
|
|
|
|
|
function closeModal() {{ |
|
|
document.getElementById('modal').style.display = 'none'; |
|
|
document.getElementById('modal-video').pause(); |
|
|
}} |
|
|
|
|
|
// Close modal when clicking outside content |
|
|
window.onclick = function(event) {{ |
|
|
const modal = document.getElementById('modal'); |
|
|
if (event.target === modal) {{ |
|
|
closeModal(); |
|
|
}} |
|
|
}} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
@app.get("/album/{album_id}/download") |
|
|
async def download_album(album_id: str): |
|
|
album = await get_album_from_db(album_id) |
|
|
if not album or 'files' not in album: |
|
|
return {"error": "Album not found or invalid data"}, 404 |
|
|
|
|
|
zip_buffer = io.BytesIO() |
|
|
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: |
|
|
for file in album['files']: |
|
|
response = requests.get(f"https://replicate.delivery/pbxt/{file['path']}") |
|
|
zip_file.writestr(file['filename'], response.content) |
|
|
|
|
|
zip_buffer.seek(0) |
|
|
|
|
|
return StreamingResponse( |
|
|
io.BytesIO(zip_buffer.getvalue()), |
|
|
media_type="application/zip", |
|
|
headers={ |
|
|
"Content-Type": "application/zip", |
|
|
"Content-Disposition": f"attachment; filename={album['albumName']}.zip" |
|
|
} |
|
|
) |
|
|
|
|
|
@app.get("/search", response_class=HTMLResponse) |
|
|
async def search_albums(query: str): |
|
|
db_albums = await get_albums_from_db() |
|
|
|
|
|
matching_albums = [ |
|
|
album for album in db_albums['data'] |
|
|
if query.lower() in album.get('albumName', '').lower() and not album.get('isPrivate', False) |
|
|
] |
|
|
|
|
|
results_html = "" |
|
|
for album in matching_albums: |
|
|
results_html += f""" |
|
|
<div class="album-item"> |
|
|
<h3>{album['albumName']}</h3> |
|
|
<p>Created: {album['createdAt']}</p> |
|
|
<p>Files: {len(album['files'])}</p> |
|
|
<a href="/album/{album['albumLink']}" class="button">View Album</a> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
return f""" |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Search Results</title> |
|
|
</head> |
|
|
<body> |
|
|
<h1>Search Results</h1> |
|
|
<p>Found {len(matching_albums)} albums for "{query}"</p> |
|
|
<div class="search-results"> |
|
|
{results_html} |
|
|
</div> |
|
|
<a href="/" class="button">← Back to Home</a> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
@app.get("/upload/{path:path}") |
|
|
async def handle_stream(path: str, request: Request): |
|
|
original_url = f'https://replicate.delivery/pbxt/{path}' |
|
|
range_header = request.headers.get('Range') |
|
|
headers = {'Range': range_header} if range_header else {} |
|
|
|
|
|
response = requests.get(original_url, headers=headers, stream=True) |
|
|
|
|
|
def generate(): |
|
|
for chunk in response.iter_content(chunk_size=8192): |
|
|
yield chunk |
|
|
|
|
|
headers = dict(response.headers) |
|
|
headers['Access-Control-Allow-Origin'] = '*' |
|
|
|
|
|
return StreamingResponse(generate(), headers=headers, status_code=response.status_code) |
|
|
|
|
|
async def get_cookies(): |
|
|
try: |
|
|
response = requests.get('https://replicate.com/levelsio/neon-tokyo', headers={ |
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' |
|
|
}) |
|
|
return dict(response.cookies) |
|
|
except Exception: |
|
|
return {} |
|
|
|
|
|
async def initiate_upload(cookies, filename, content_type): |
|
|
url = f"https://replicate.com/api/upload/{filename}?content_type={content_type}" |
|
|
headers = { |
|
|
'X-CSRFToken': cookies['csrftoken'], |
|
|
'Referer': 'https://replicate.com/levelsio/neon-tokyo', |
|
|
'Origin': 'https://replicate.com' |
|
|
} |
|
|
response = requests.post(url, cookies=cookies, headers=headers) |
|
|
return response.json() |
|
|
|
|
|
async def retry_upload(upload_url, file_content, content_type, max_retries=5): |
|
|
delay = 1 |
|
|
for _ in range(max_retries): |
|
|
try: |
|
|
response = requests.put(upload_url, data=file_content, headers={'Content-Type': content_type}) |
|
|
if response.status_code == 200: |
|
|
return True |
|
|
except Exception: |
|
|
pass |
|
|
await asyncio.sleep(delay) |
|
|
delay *= 2 |
|
|
return False |
|
|
|
|
|
if __name__ == "__main__": |
|
|
import uvicorn |
|
|
uvicorn.run(app, host="0.0.0.0", port=8000) |