Download Stems button now zips all stem files into a single .zip

- New /admin/request/<id>/download-stems endpoint
- Parses stems_url (label: url; label: url format), fetches each stem file,
  bundles them into a zip served as an attachment download
- If MusicGPT returns a single .zip bundle URL, redirects directly
- Template button now points to the zip endpoint instead of raw CDN URLs
This commit is contained in:
Troll (Hermes Agent) 2026-08-11 19:36:40 +00:00
parent c676acd32f
commit f2c64848dd
2 changed files with 73 additions and 1 deletions

72
app.py
View file

@ -34,6 +34,9 @@ import hmac
import shutil import shutil
import time import time
import json import json
import io
import zipfile
import re
from pathlib import Path from pathlib import Path
# Flask and related imports # Flask and related imports
@ -790,6 +793,75 @@ def audio(token, version):
abort(404) abort(404)
@app.route('/admin/request/<int:rid>/download-stems')
def admin_download_stems(rid):
"""Download all stems files for a request as a single .zip archive."""
redir = require_admin()
if redir:
return redir
req = get_request_by_id(rid)
if not req:
abort(404)
req = dict(req)
stems_url = req.get('stems_url') or ''
if not stems_url:
flash('No stems available to download.', 'error')
return redirect(url_for('admin_request', rid=rid))
# Parse the stems_url field. It can be:
# 1. A single bundle/zip URL (use directly)
# 2. "label1: url1; label2: url2" (multiple individual stem files)
# 3. Just a bare URL
urls = []
if ';' in stems_url or ': ' in stems_url:
# Multiple stems: "instrumental: https://...; vocals: https://..."
parts = stems_url.split(';')
for part in parts:
part = part.strip()
if not part:
continue
# Split on first ": " to separate label from URL
if ': ' in part:
label, url = part.split(': ', 1)
urls.append((label.strip(), url.strip()))
else:
urls.append(('stem', part))
else:
urls.append(('stems', stems_url.strip()))
if not urls:
flash('No stems files found.', 'error')
return redirect(url_for('admin_request', rid=rid))
# If there's only one URL and it's already a .zip, redirect directly.
if len(urls) == 1 and urls[0][1].endswith('.zip'):
return redirect(urls[0][1])
# Download each stem file and bundle them into a zip in memory.
import requests as req_lib
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
for label, url in urls:
try:
r = req_lib.get(url, timeout=60)
r.raise_for_status()
# Derive a filename from the URL or label.
filename = url.split('/')[-1] if '/' in url else f"{label}.mp3"
# If the URL has no extension, use the label.
if '.' not in filename:
filename = f"{label}.mp3"
zf.writestr(filename, r.content)
except Exception:
# Skip files that fail to download, but include the rest.
pass
buf.seek(0)
title = req.get('suno_title') or req.get('name') or f'request_{rid}'
download_name = f"stems_{title}.zip"
return send_file(buf, mimetype='application/zip', as_attachment=True, download_name=download_name)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Admin routes # Admin routes
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -618,7 +618,7 @@
{% endif %} {% endif %}
{% if req.stems_url %} {% if req.stems_url %}
<div class="actions"> <div class="actions">
<a href="{{ req.stems_url }}" target="_blank" rel="noopener noreferrer" class="button-link" style="display:inline-flex;align-items:center;gap:.4rem;padding:.5rem 1rem;background:#4b5563;border-radius:.375rem;color:#fff;text-decoration:none;font-size:.875rem">⬇ Download Stems</a> <a href="{{ url_for('admin_download_stems', rid=req.id) }}" class="button-link" style="display:inline-flex;align-items:center;gap:.4rem;padding:.5rem 1rem;background:#4b5563;border-radius:.375rem;color:#fff;text-decoration:none;font-size:.875rem">⬇ Download Stems (zip)</a>
</div> </div>
{% endif %} {% endif %}