file / app.py
albumup's picture
Update app.py
c1e283b verified
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}%")
# Save to Deno KV database
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">&times;</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()
# Exclude private albums from search results
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)