feat: add per-song MP3 download buttons to admin request page
Adds an admin download route (/admin/request/<rid>/download-song/<version>) and Download buttons next to Version A and Version B in section 2 (Songs), shown only when the MP3 exists on disk. Bumps version to 0.9.2.
This commit is contained in:
parent
530451cfef
commit
be85a88543
5 changed files with 34 additions and 3 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Theme Song Booth (MusicGPT Edition)
|
# Theme Song Booth (MusicGPT Edition)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
A Flask web application for running a convention booth where visitors request a custom AI-generated theme song. Operators manage the queue from an admin dashboard, generate prompts via Hermes, queue generations through the MusicGPT API, and deliver final songs by email.
|
A Flask web application for running a convention booth where visitors request a custom AI-generated theme song. Operators manage the queue from an admin dashboard, generate prompts via Hermes, queue generations through the MusicGPT API, and deliver final songs by email.
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ The selected pronouns are stored in the `pronouns` column and included in confir
|
||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| Login | `/admin/login` | Simple session-based login. Password comes from the `ADMIN_PASSWORD` environment variable. |
|
| Login | `/admin/login` | Simple session-based login. Password comes from the `ADMIN_PASSWORD` environment variable. |
|
||||||
| Dashboard | `/admin` | Main queue. Filter by status and auto-refresh at a configurable interval. Includes a **Refresh MusicGPT Status** button to manually poll all in-flight tasks. |
|
| Dashboard | `/admin` | Main queue. Filter by status and auto-refresh at a configurable interval. Includes a **Refresh MusicGPT Status** button to manually poll all in-flight tasks. |
|
||||||
| Request detail | `/admin/request/<id>` | Full control of one request: edit customer info, save prompt, copy Hermes callback, queue MusicGPT generation, poll a single MusicGPT task, view status/cost, view revision history, upload MP3 overrides, send preview, record payment, deliver files (MP3/WAV/cover), add operator notes, cancel generation, and generate STEMS. |
|
| Request detail | `/admin/request/<id>` | Full control of one request: edit customer info, save prompt, copy Hermes callback, queue MusicGPT generation, poll a single MusicGPT task, view status/cost, view revision history, upload MP3 overrides, download individual Version A/B MP3s, send preview, record payment, deliver files (MP3/WAV/cover), add operator notes, cancel generation, and generate STEMS. |
|
||||||
| Pricing | `/admin/pricing` | Configure fixed prices and up to 5 custom items. Also shows running MusicGPT cost totals in USD. |
|
| Pricing | `/admin/pricing` | Configure fixed prices and up to 5 custom items. Also shows running MusicGPT cost totals in USD. |
|
||||||
| Sales | `/admin/sales` | Report of all delivered requests with customer details and Square payment references. |
|
| Sales | `/admin/sales` | Report of all delivered requests with customer details and Square payment references. |
|
||||||
| Settings | `/admin/settings` | Database health, backups, SMTP config, MP3 metadata defaults, revision limit, auto-refresh interval, kiosk mode, booth open/closed switch, Hermes API key management, MusicGPT webhook URL, album cover delivery toggle, automatic MusicGPT polling toggle, and system reset. |
|
| Settings | `/admin/settings` | Database health, backups, SMTP config, MP3 metadata defaults, revision limit, auto-refresh interval, kiosk mode, booth open/closed switch, Hermes API key management, MusicGPT webhook URL, album cover delivery toggle, automatic MusicGPT polling toggle, and system reset. |
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.9.1
|
0.9.2
|
||||||
|
|
|
||||||
18
app.py
18
app.py
|
|
@ -922,6 +922,24 @@ def admin_download_stems(rid):
|
||||||
return send_file(buf, mimetype='application/zip', as_attachment=True, download_name=download_name)
|
return send_file(buf, mimetype='application/zip', as_attachment=True, download_name=download_name)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/admin/request/<int:rid>/download-song/<version>')
|
||||||
|
def admin_download_song(rid, version):
|
||||||
|
"""Download a single song MP3 (Version A or B) for a request."""
|
||||||
|
redir = require_admin()
|
||||||
|
if redir:
|
||||||
|
return redir
|
||||||
|
req = get_request_by_id(rid)
|
||||||
|
if not req:
|
||||||
|
abort(404)
|
||||||
|
if version not in ('a', 'b'):
|
||||||
|
abort(404)
|
||||||
|
path = req.get('song_a_path') if version == 'a' else req.get('song_b_path')
|
||||||
|
if not path or not Path(path).exists():
|
||||||
|
flash('Song file not found.', 'error')
|
||||||
|
return redirect(url_for('admin_request', rid=rid))
|
||||||
|
return send_file(path, as_attachment=True, download_name=Path(path).name)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Admin routes
|
# Admin routes
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -558,6 +558,13 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.booth-page .song-download {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
.booth-page .file-select {
|
.booth-page .file-select {
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,9 @@
|
||||||
<span class="status-badge missing">Not ready</span>
|
<span class="status-badge missing">Not ready</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</label>
|
</label>
|
||||||
|
{% if req.song_a_path and file_exists(req.song_a_path) %}
|
||||||
|
<a href="{{ url_for('admin_download_song', rid=req.id, version='a') }}" class="button-link song-download">⬇ Download</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="song-row">
|
<div class="song-row">
|
||||||
<input type="checkbox" id="delete_b" name="delete_song" value="b" {% if not (req.song_b_path and file_exists(req.song_b_path)) %}disabled{% endif %}>
|
<input type="checkbox" id="delete_b" name="delete_song" value="b" {% if not (req.song_b_path and file_exists(req.song_b_path)) %}disabled{% endif %}>
|
||||||
|
|
@ -326,6 +329,9 @@
|
||||||
<span class="status-badge missing">Not ready</span>
|
<span class="status-badge missing">Not ready</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</label>
|
</label>
|
||||||
|
{% if req.song_b_path and file_exists(req.song_b_path) %}
|
||||||
|
<a href="{{ url_for('admin_download_song', rid=req.id, version='b') }}" class="button-link song-download">⬇ Download</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" class="button-danger" onclick="return confirm('Delete selected song files? This will reset the request to Prompt Ready.')">Delete Selected Songs</button>
|
<button type="submit" class="button-danger" onclick="return confirm('Delete selected song files? This will reset the request to Prompt Ready.')">Delete Selected Songs</button>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue