Add content negotiation to /cache/{content_hash}
- Browsers get HTML detail page with video/image preview
- API clients with Accept: application/json get metadata JSON
- Other requests get raw file
- Add /cache/{content_hash}/raw for explicit file downloads
- Remove old /cache/{content_hash}/detail endpoint
- Update all /detail links to use clean /cache/{hash} URL
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
357
server.py
357
server.py
@@ -796,7 +796,7 @@ async def run_detail(run_id: str, request: Request):
|
|||||||
media_html += f'''
|
media_html += f'''
|
||||||
<div class="bg-dark-600 rounded-lg p-4">
|
<div class="bg-dark-600 rounded-lg p-4">
|
||||||
<div class="text-sm text-gray-400 mb-2">Input</div>
|
<div class="text-sm text-gray-400 mb-2">Input</div>
|
||||||
<a href="/cache/{input_hash}/detail" class="text-blue-400 hover:text-blue-300 font-mono text-xs">{input_hash[:24]}...</a>
|
<a href="/cache/{input_hash}" class="text-blue-400 hover:text-blue-300 font-mono text-xs">{input_hash[:24]}...</a>
|
||||||
<div class="mt-3 flex justify-center">{input_elem}</div>
|
<div class="mt-3 flex justify-center">{input_elem}</div>
|
||||||
</div>
|
</div>
|
||||||
'''
|
'''
|
||||||
@@ -813,14 +813,14 @@ async def run_detail(run_id: str, request: Request):
|
|||||||
media_html += f'''
|
media_html += f'''
|
||||||
<div class="bg-dark-600 rounded-lg p-4">
|
<div class="bg-dark-600 rounded-lg p-4">
|
||||||
<div class="text-sm text-gray-400 mb-2">Output</div>
|
<div class="text-sm text-gray-400 mb-2">Output</div>
|
||||||
<a href="/cache/{output_hash}/detail" class="text-blue-400 hover:text-blue-300 font-mono text-xs">{output_hash[:24]}...</a>
|
<a href="/cache/{output_hash}" class="text-blue-400 hover:text-blue-300 font-mono text-xs">{output_hash[:24]}...</a>
|
||||||
<div class="mt-3 flex justify-center">{output_elem}</div>
|
<div class="mt-3 flex justify-center">{output_elem}</div>
|
||||||
</div>
|
</div>
|
||||||
'''
|
'''
|
||||||
media_html += '</div>'
|
media_html += '</div>'
|
||||||
|
|
||||||
# Build inputs list
|
# Build inputs list
|
||||||
inputs_html = ''.join([f'<a href="/cache/{inp}/detail" class="text-blue-400 hover:text-blue-300 font-mono text-xs block">{inp}</a>' for inp in run.inputs])
|
inputs_html = ''.join([f'<a href="/cache/{inp}" class="text-blue-400 hover:text-blue-300 font-mono text-xs block">{inp}</a>' for inp in run.inputs])
|
||||||
|
|
||||||
# Infrastructure section
|
# Infrastructure section
|
||||||
infra_html = ""
|
infra_html = ""
|
||||||
@@ -888,7 +888,7 @@ async def run_detail(run_id: str, request: Request):
|
|||||||
if run.output_hash:
|
if run.output_hash:
|
||||||
output_link = f'''<div class="bg-dark-600 rounded-lg p-4">
|
output_link = f'''<div class="bg-dark-600 rounded-lg p-4">
|
||||||
<div class="text-sm text-gray-400 mb-1">Output</div>
|
<div class="text-sm text-gray-400 mb-1">Output</div>
|
||||||
<a href="/cache/{run.output_hash}/detail" class="text-blue-400 hover:text-blue-300 font-mono text-xs">{run.output_hash}</a>
|
<a href="/cache/{run.output_hash}" class="text-blue-400 hover:text-blue-300 font-mono text-xs">{run.output_hash}</a>
|
||||||
</div>'''
|
</div>'''
|
||||||
|
|
||||||
completed_html = ""
|
completed_html = ""
|
||||||
@@ -1654,8 +1654,179 @@ async def ui_discard_recipe(recipe_id: str, request: Request):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/cache/{content_hash}")
|
@app.get("/cache/{content_hash}")
|
||||||
async def get_cached(content_hash: str):
|
async def get_cached(content_hash: str, request: Request):
|
||||||
"""Get cached content by hash."""
|
"""Get cached content by hash. Content negotiation: HTML for browsers, JSON for APIs, file for downloads."""
|
||||||
|
ctx = get_user_context_from_cookie(request)
|
||||||
|
cache_path = get_cache_path(content_hash)
|
||||||
|
|
||||||
|
if not cache_path:
|
||||||
|
if wants_html(request):
|
||||||
|
content = f'<p class="text-red-400">Content not found: {content_hash}</p>'
|
||||||
|
return HTMLResponse(render_page("Not Found", content, ctx.actor_id if ctx else None, active_tab="media"), status_code=404)
|
||||||
|
raise HTTPException(404, f"Content {content_hash} not in cache")
|
||||||
|
|
||||||
|
# JSON response for API clients
|
||||||
|
accept = request.headers.get("accept", "")
|
||||||
|
if "application/json" in accept:
|
||||||
|
meta = await database.load_item_metadata(content_hash, ctx.actor_id if ctx else None)
|
||||||
|
cache_item = await database.get_cache_item(content_hash)
|
||||||
|
ipfs_cid = cache_item.get("ipfs_cid") if cache_item else None
|
||||||
|
file_size = cache_path.stat().st_size
|
||||||
|
media_type = detect_media_type(cache_path)
|
||||||
|
return {
|
||||||
|
"content_hash": content_hash,
|
||||||
|
"size": file_size,
|
||||||
|
"media_type": media_type,
|
||||||
|
"ipfs_cid": ipfs_cid,
|
||||||
|
"meta": meta
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTML response for browsers - show detail page
|
||||||
|
if wants_html(request):
|
||||||
|
if not ctx:
|
||||||
|
content = '<p class="text-gray-400 py-8 text-center"><a href="/login" class="text-blue-400 hover:text-blue-300">Login via L2</a> to view cached content.</p>'
|
||||||
|
return HTMLResponse(render_page("Login Required", content, None, active_tab="media"), status_code=401)
|
||||||
|
|
||||||
|
# Check user has access
|
||||||
|
user_hashes = await get_user_cache_hashes(ctx.username, ctx.actor_id)
|
||||||
|
if content_hash not in user_hashes:
|
||||||
|
content = '<p class="text-red-400 py-8 text-center">Access denied.</p>'
|
||||||
|
return HTMLResponse(render_page("Access Denied", content, ctx.actor_id, active_tab="media"), status_code=403)
|
||||||
|
|
||||||
|
media_type = detect_media_type(cache_path)
|
||||||
|
file_size = cache_path.stat().st_size
|
||||||
|
size_str = f"{file_size:,} bytes"
|
||||||
|
if file_size > 1024*1024:
|
||||||
|
size_str = f"{file_size/(1024*1024):.1f} MB"
|
||||||
|
elif file_size > 1024:
|
||||||
|
size_str = f"{file_size/1024:.1f} KB"
|
||||||
|
|
||||||
|
# Get IPFS CID from database
|
||||||
|
cache_item = await database.get_cache_item(content_hash)
|
||||||
|
ipfs_cid = cache_item.get("ipfs_cid") if cache_item else None
|
||||||
|
|
||||||
|
# Build media display HTML
|
||||||
|
if media_type == "video":
|
||||||
|
video_src = video_src_for_request(content_hash, request)
|
||||||
|
media_html = f'<video src="{video_src}" controls autoplay muted loop playsinline class="max-w-full max-h-96 rounded-lg"></video>'
|
||||||
|
elif media_type == "image":
|
||||||
|
media_html = f'<img src="/cache/{content_hash}/raw" alt="{content_hash}" class="max-w-full max-h-96 rounded-lg">'
|
||||||
|
else:
|
||||||
|
media_html = f'<p class="text-gray-400">Unknown file type. <a href="/cache/{content_hash}/raw" download class="text-blue-400 hover:text-blue-300">Download file</a></p>'
|
||||||
|
|
||||||
|
content = f'''
|
||||||
|
<a href="/media" class="inline-flex items-center text-blue-400 hover:text-blue-300 mb-6">
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
Back to media
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="bg-dark-700 rounded-lg p-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="px-3 py-1 bg-blue-600 text-white text-sm font-medium rounded-full">{media_type.capitalize()}</span>
|
||||||
|
<span class="text-gray-400 font-mono text-sm">{content_hash[:24]}...</span>
|
||||||
|
</div>
|
||||||
|
<a href="/cache/{content_hash}/raw" download
|
||||||
|
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center mb-8">
|
||||||
|
{media_html}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-dark-500 pt-6">
|
||||||
|
<h2 class="text-lg font-semibold text-white mb-4">Details</h2>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div class="bg-dark-600 rounded-lg p-4">
|
||||||
|
<div class="text-sm text-gray-400 mb-1">Content Hash (SHA3-256)</div>
|
||||||
|
<div class="font-mono text-xs text-gray-200 break-all">{content_hash}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-dark-600 rounded-lg p-4">
|
||||||
|
<div class="text-sm text-gray-400 mb-1">Type</div>
|
||||||
|
<div class="text-gray-200">{media_type}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-dark-600 rounded-lg p-4">
|
||||||
|
<div class="text-sm text-gray-400 mb-1">Size</div>
|
||||||
|
<div class="text-gray-200">{size_str}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-dark-600 rounded-lg p-4">
|
||||||
|
<div class="text-sm text-gray-400 mb-1">Raw URL</div>
|
||||||
|
<div class="text-blue-400 text-sm truncate">
|
||||||
|
<a href="/cache/{content_hash}/raw" class="hover:text-blue-300">/cache/{content_hash}/raw</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Add IPFS section if we have a CID
|
||||||
|
if ipfs_cid:
|
||||||
|
gateway_links = []
|
||||||
|
if IPFS_GATEWAY_URL:
|
||||||
|
gateway_links.append(f'''
|
||||||
|
<a href="{IPFS_GATEWAY_URL}/ipfs/{ipfs_cid}" target="_blank" rel="noopener"
|
||||||
|
class="px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors">
|
||||||
|
Local Gateway
|
||||||
|
</a>''')
|
||||||
|
gateway_links.extend([
|
||||||
|
f'''<a href="https://ipfs.io/ipfs/{ipfs_cid}" target="_blank" rel="noopener"
|
||||||
|
class="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-colors">
|
||||||
|
ipfs.io
|
||||||
|
</a>''',
|
||||||
|
f'''<a href="https://dweb.link/ipfs/{ipfs_cid}" target="_blank" rel="noopener"
|
||||||
|
class="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-colors">
|
||||||
|
dweb.link
|
||||||
|
</a>''',
|
||||||
|
f'''<a href="https://cloudflare-ipfs.com/ipfs/{ipfs_cid}" target="_blank" rel="noopener"
|
||||||
|
class="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-colors">
|
||||||
|
Cloudflare
|
||||||
|
</a>''',
|
||||||
|
])
|
||||||
|
gateways_html = '\n'.join(gateway_links)
|
||||||
|
|
||||||
|
content += f'''
|
||||||
|
<div class="border-t border-dark-500 pt-6 mt-6">
|
||||||
|
<h2 class="text-lg font-semibold text-white mb-4">IPFS</h2>
|
||||||
|
<div class="bg-dark-600 rounded-lg p-4 mb-4">
|
||||||
|
<div class="text-sm text-gray-400 mb-1">Content Identifier (CID)</div>
|
||||||
|
<div class="font-mono text-xs text-gray-200 break-all">{ipfs_cid}</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-400 mb-2">Gateways:</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{gateways_html}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
else:
|
||||||
|
content += '''
|
||||||
|
<div class="border-t border-dark-500 pt-6 mt-6">
|
||||||
|
<h2 class="text-lg font-semibold text-white mb-4">IPFS</h2>
|
||||||
|
<div class="text-gray-400 text-sm">Not yet uploaded to IPFS</div>
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
|
||||||
|
content += f'''
|
||||||
|
<!-- Metadata Section -->
|
||||||
|
<div class="border-t border-dark-500 pt-6 mt-6" id="metadata-section"
|
||||||
|
hx-get="/cache/{content_hash}/meta-form" hx-trigger="load" hx-swap="innerHTML">
|
||||||
|
<div class="text-gray-400">Loading metadata...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
|
||||||
|
return HTMLResponse(render_page(f"Cache: {content_hash[:16]}...", content, ctx.actor_id, active_tab="media"))
|
||||||
|
|
||||||
|
# Default: return raw file
|
||||||
|
return FileResponse(cache_path)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/cache/{content_hash}/raw")
|
||||||
|
async def get_cached_raw(content_hash: str):
|
||||||
|
"""Get raw cached content (file download)."""
|
||||||
cache_path = get_cache_path(content_hash)
|
cache_path = get_cache_path(content_hash)
|
||||||
if not cache_path:
|
if not cache_path:
|
||||||
raise HTTPException(404, f"Content {content_hash} not in cache")
|
raise HTTPException(404, f"Content {content_hash} not in cache")
|
||||||
@@ -1724,172 +1895,6 @@ async def get_cached_mp4(content_hash: str):
|
|||||||
return FileResponse(mp4_path, media_type="video/mp4")
|
return FileResponse(mp4_path, media_type="video/mp4")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/cache/{content_hash}/detail")
|
|
||||||
async def cache_detail(content_hash: str, request: Request):
|
|
||||||
"""View cached content detail. HTML for browsers, JSON for APIs."""
|
|
||||||
ctx = get_user_context_from_cookie(request)
|
|
||||||
|
|
||||||
cache_path = get_cache_path(content_hash)
|
|
||||||
if not cache_path:
|
|
||||||
if wants_html(request):
|
|
||||||
content = f'<p class="text-red-400">Content not found: {content_hash}</p>'
|
|
||||||
return HTMLResponse(render_page("Not Found", content, ctx.actor_id if ctx else None, active_tab="media"), status_code=404)
|
|
||||||
raise HTTPException(404, f"Content {content_hash} not in cache")
|
|
||||||
|
|
||||||
if wants_html(request):
|
|
||||||
if not ctx:
|
|
||||||
content = '<p class="text-gray-400 py-8 text-center"><a href="/login" class="text-blue-400 hover:text-blue-300">Login via L2</a> to view cached content.</p>'
|
|
||||||
return HTMLResponse(render_page("Login Required", content, None, active_tab="media"), status_code=401)
|
|
||||||
|
|
||||||
# Check user has access
|
|
||||||
user_hashes = await get_user_cache_hashes(ctx.username, ctx.actor_id)
|
|
||||||
if content_hash not in user_hashes:
|
|
||||||
content = '<p class="text-red-400 py-8 text-center">Access denied.</p>'
|
|
||||||
return HTMLResponse(render_page("Access Denied", content, ctx.actor_id, active_tab="media"), status_code=403)
|
|
||||||
|
|
||||||
media_type = detect_media_type(cache_path)
|
|
||||||
file_size = cache_path.stat().st_size
|
|
||||||
size_str = f"{file_size:,} bytes"
|
|
||||||
if file_size > 1024*1024:
|
|
||||||
size_str = f"{file_size/(1024*1024):.1f} MB"
|
|
||||||
elif file_size > 1024:
|
|
||||||
size_str = f"{file_size/1024:.1f} KB"
|
|
||||||
|
|
||||||
# Get IPFS CID from database
|
|
||||||
cache_item = await database.get_cache_item(content_hash)
|
|
||||||
ipfs_cid = cache_item.get("ipfs_cid") if cache_item else None
|
|
||||||
|
|
||||||
# Build media display HTML
|
|
||||||
if media_type == "video":
|
|
||||||
video_src = video_src_for_request(content_hash, request)
|
|
||||||
media_html = f'<video src="{video_src}" controls autoplay muted loop playsinline class="max-w-full max-h-96 rounded-lg"></video>'
|
|
||||||
elif media_type == "image":
|
|
||||||
media_html = f'<img src="/cache/{content_hash}" alt="{content_hash}" class="max-w-full max-h-96 rounded-lg">'
|
|
||||||
else:
|
|
||||||
media_html = f'<p class="text-gray-400">Unknown file type. <a href="/cache/{content_hash}" download class="text-blue-400 hover:text-blue-300">Download file</a></p>'
|
|
||||||
|
|
||||||
content = f'''
|
|
||||||
<a href="/media" class="inline-flex items-center text-blue-400 hover:text-blue-300 mb-6">
|
|
||||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
||||||
</svg>
|
|
||||||
Back to media
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="bg-dark-700 rounded-lg p-6">
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="px-3 py-1 bg-blue-600 text-white text-sm font-medium rounded-full">{media_type.capitalize()}</span>
|
|
||||||
<span class="text-gray-400 font-mono text-sm">{content_hash[:24]}...</span>
|
|
||||||
</div>
|
|
||||||
<a href="/cache/{content_hash}" download
|
|
||||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-center mb-8">
|
|
||||||
{media_html}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border-t border-dark-500 pt-6">
|
|
||||||
<h2 class="text-lg font-semibold text-white mb-4">Details</h2>
|
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div class="bg-dark-600 rounded-lg p-4">
|
|
||||||
<div class="text-sm text-gray-400 mb-1">Content Hash (SHA3-256)</div>
|
|
||||||
<div class="font-mono text-xs text-gray-200 break-all">{content_hash}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-dark-600 rounded-lg p-4">
|
|
||||||
<div class="text-sm text-gray-400 mb-1">Type</div>
|
|
||||||
<div class="text-gray-200">{media_type}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-dark-600 rounded-lg p-4">
|
|
||||||
<div class="text-sm text-gray-400 mb-1">Size</div>
|
|
||||||
<div class="text-gray-200">{size_str}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-dark-600 rounded-lg p-4">
|
|
||||||
<div class="text-sm text-gray-400 mb-1">Raw URL</div>
|
|
||||||
<div class="text-blue-400 text-sm truncate">
|
|
||||||
<a href="/cache/{content_hash}" class="hover:text-blue-300">/cache/{content_hash}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Add IPFS section if we have a CID
|
|
||||||
if ipfs_cid:
|
|
||||||
# Build gateway links - local gateway first if configured
|
|
||||||
gateway_links = []
|
|
||||||
if IPFS_GATEWAY_URL:
|
|
||||||
gateway_links.append(f'''
|
|
||||||
<a href="{IPFS_GATEWAY_URL}/ipfs/{ipfs_cid}" target="_blank" rel="noopener"
|
|
||||||
class="px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors">
|
|
||||||
Local Gateway
|
|
||||||
</a>''')
|
|
||||||
gateway_links.extend([
|
|
||||||
f'''<a href="https://ipfs.io/ipfs/{ipfs_cid}" target="_blank" rel="noopener"
|
|
||||||
class="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-colors">
|
|
||||||
ipfs.io
|
|
||||||
</a>''',
|
|
||||||
f'''<a href="https://dweb.link/ipfs/{ipfs_cid}" target="_blank" rel="noopener"
|
|
||||||
class="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-colors">
|
|
||||||
dweb.link
|
|
||||||
</a>''',
|
|
||||||
f'''<a href="https://cloudflare-ipfs.com/ipfs/{ipfs_cid}" target="_blank" rel="noopener"
|
|
||||||
class="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-colors">
|
|
||||||
Cloudflare
|
|
||||||
</a>''',
|
|
||||||
])
|
|
||||||
gateways_html = '\n'.join(gateway_links)
|
|
||||||
|
|
||||||
content += f'''
|
|
||||||
<div class="border-t border-dark-500 pt-6 mt-6">
|
|
||||||
<h2 class="text-lg font-semibold text-white mb-4">IPFS</h2>
|
|
||||||
<div class="bg-dark-600 rounded-lg p-4 mb-4">
|
|
||||||
<div class="text-sm text-gray-400 mb-1">Content Identifier (CID)</div>
|
|
||||||
<div class="font-mono text-xs text-gray-200 break-all">{ipfs_cid}</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-400 mb-2">Gateways:</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
{gateways_html}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
'''
|
|
||||||
else:
|
|
||||||
content += '''
|
|
||||||
<div class="border-t border-dark-500 pt-6 mt-6">
|
|
||||||
<h2 class="text-lg font-semibold text-white mb-4">IPFS</h2>
|
|
||||||
<div class="text-gray-400 text-sm">Not yet uploaded to IPFS</div>
|
|
||||||
</div>
|
|
||||||
'''
|
|
||||||
|
|
||||||
content += f'''
|
|
||||||
<!-- Metadata Section -->
|
|
||||||
<div class="border-t border-dark-500 pt-6 mt-6" id="metadata-section"
|
|
||||||
hx-get="/cache/{content_hash}/meta-form" hx-trigger="load" hx-swap="innerHTML">
|
|
||||||
<div class="text-gray-400">Loading metadata...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
'''
|
|
||||||
|
|
||||||
return HTMLResponse(render_page(f"Cache: {content_hash[:16]}...", content, ctx.actor_id, active_tab="media"))
|
|
||||||
|
|
||||||
# JSON response - return metadata
|
|
||||||
meta = await database.load_item_metadata(content_hash, ctx.actor_id if ctx else None)
|
|
||||||
cache_item = await database.get_cache_item(content_hash)
|
|
||||||
ipfs_cid = cache_item.get("ipfs_cid") if cache_item else None
|
|
||||||
file_size = cache_path.stat().st_size
|
|
||||||
media_type = detect_media_type(cache_path)
|
|
||||||
return {
|
|
||||||
"content_hash": content_hash,
|
|
||||||
"size": file_size,
|
|
||||||
"media_type": media_type,
|
|
||||||
"ipfs_cid": ipfs_cid,
|
|
||||||
"meta": meta
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/cache/{content_hash}/meta-form", response_class=HTMLResponse)
|
@app.get("/cache/{content_hash}/meta-form", response_class=HTMLResponse)
|
||||||
async def cache_meta_form(content_hash: str, request: Request):
|
async def cache_meta_form(content_hash: str, request: Request):
|
||||||
"""Clean URL redirect to the HTMX meta form."""
|
"""Clean URL redirect to the HTMX meta form."""
|
||||||
@@ -1898,12 +1903,6 @@ async def cache_meta_form(content_hash: str, request: Request):
|
|||||||
return RedirectResponse(f"/ui/cache/{content_hash}/meta-form", status_code=302)
|
return RedirectResponse(f"/ui/cache/{content_hash}/meta-form", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/ui/cache/{content_hash}")
|
|
||||||
async def ui_cache_view(content_hash: str):
|
|
||||||
"""Redirect to clean URL."""
|
|
||||||
return RedirectResponse(url=f"/cache/{content_hash}/detail", status_code=302)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/ui/cache/{content_hash}/meta-form", response_class=HTMLResponse)
|
@app.get("/ui/cache/{content_hash}/meta-form", response_class=HTMLResponse)
|
||||||
async def ui_cache_meta_form(content_hash: str, request: Request):
|
async def ui_cache_meta_form(content_hash: str, request: Request):
|
||||||
"""HTMX partial: metadata editing form for a cached item."""
|
"""HTMX partial: metadata editing form for a cached item."""
|
||||||
@@ -2387,7 +2386,7 @@ async def list_media(
|
|||||||
size_str = f"{size} bytes"
|
size_str = f"{size} bytes"
|
||||||
|
|
||||||
html_parts.append(f'''
|
html_parts.append(f'''
|
||||||
<a href="/cache/{content_hash}/detail" class="block">
|
<a href="/cache/{content_hash}" class="block">
|
||||||
<div class="bg-dark-700 rounded-lg p-4 hover:bg-dark-600 transition-colors">
|
<div class="bg-dark-700 rounded-lg p-4 hover:bg-dark-600 transition-colors">
|
||||||
<div class="flex items-center justify-between gap-2 mb-3">
|
<div class="flex items-center justify-between gap-2 mb-3">
|
||||||
<span class="px-2 py-1 bg-blue-600 text-white text-xs font-medium rounded-full">{media_type}</span>
|
<span class="px-2 py-1 bg-blue-600 text-white text-xs font-medium rounded-full">{media_type}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user