Auto-upload stems to Gokapi on completion, save share link
- New helpers: parse_stems_urls, upload_to_gokapi, process_stems_to_gokapi - Stems webhook handler now downloads stem files from MusicGPT CDN, zips them, uploads to Gokapi with 30-day expiry, saves the Gokapi download URL to stems_link in the DB - Download Stems button uses stems_link (Gokapi URL) when available, falls back to the zip endpoint if not - Config: GOKAPI_URL, GOKAPI_API_KEY, GOKAPI_EXPIRY_DAYS env vars - docker-compose: GOKAPI_* environment variables added
This commit is contained in:
parent
9df4c685a4
commit
ccc93ffb3b
5 changed files with 140 additions and 3 deletions
12
app.py
12
app.py
|
|
@ -66,6 +66,7 @@ from helpers import (
|
||||||
musicgpt_generate_request, musicgpt_queue_stems, musicgpt_poll_status,
|
musicgpt_generate_request, musicgpt_queue_stems, musicgpt_poll_status,
|
||||||
download_musicgpt_outputs, format_musicgpt_cost, get_musicgpt_cost_totals,
|
download_musicgpt_outputs, format_musicgpt_cost, get_musicgpt_cost_totals,
|
||||||
download_album_cover,
|
download_album_cover,
|
||||||
|
process_stems_to_gokapi,
|
||||||
get_ntfy_config, send_ntfy,
|
get_ntfy_config, send_ntfy,
|
||||||
sign_prompt_callback, verify_prompt_callback, build_prompt_callback_url,
|
sign_prompt_callback, verify_prompt_callback, build_prompt_callback_url,
|
||||||
send_email, build_signature_images,
|
send_email, build_signature_images,
|
||||||
|
|
@ -419,7 +420,6 @@ def musicgpt_webhook():
|
||||||
if not stems_url and audio_url_map:
|
if not stems_url and audio_url_map:
|
||||||
stems_url = '; '.join(f"{k}: {v}" for k, v in audio_url_map.items())
|
stems_url = '; '.join(f"{k}: {v}" for k, v in audio_url_map.items())
|
||||||
update_fields['stems_url'] = stems_url
|
update_fields['stems_url'] = stems_url
|
||||||
update_fields['stems_link'] = stems_url
|
|
||||||
try:
|
try:
|
||||||
sc = float(data.get('conversion_cost') or 0)
|
sc = float(data.get('conversion_cost') or 0)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
|
|
@ -429,6 +429,16 @@ def musicgpt_webhook():
|
||||||
elif new_status in ('FAILED', 'ERROR'):
|
elif new_status in ('FAILED', 'ERROR'):
|
||||||
update_fields['stems_error'] = data.get('reason') or data.get('error') or 'Extraction failed'
|
update_fields['stems_error'] = data.get('reason') or data.get('error') or 'Extraction failed'
|
||||||
update_request(req['id'], **update_fields)
|
update_request(req['id'], **update_fields)
|
||||||
|
|
||||||
|
# After stems are complete, upload them to Gokapi for a shareable link.
|
||||||
|
if new_status in ('COMPLETED', 'FINISHED') and Config.GOKAPI_URL and Config.GOKAPI_API_KEY:
|
||||||
|
row = db.execute('SELECT * FROM requests WHERE id = ?', (req['id'],)).fetchone()
|
||||||
|
req = dict(row)
|
||||||
|
gokapi_url, gokapi_err = process_stems_to_gokapi(req)
|
||||||
|
if gokapi_err:
|
||||||
|
app.logger.warning(f'Gokapi upload failed for request {req["id"]}: {gokapi_err}')
|
||||||
|
# Note: process_stems_to_gokapi already saves stems_link to the DB on success.
|
||||||
|
|
||||||
return jsonify({'ok': True, 'request_id': req['id']}), 200
|
return jsonify({'ok': True, 'request_id': req['id']}), 200
|
||||||
|
|
||||||
return jsonify({'ok': False, 'reason': 'unknown_conversion_type'}), 400
|
return jsonify({'ok': False, 'reason': 'unknown_conversion_type'}), 400
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,11 @@ class Config:
|
||||||
MUSICGPT_MODELS = ['v6', 'v6-pro']
|
MUSICGPT_MODELS = ['v6', 'v6-pro']
|
||||||
MUSICGPT_DEFAULT_MODEL = os.environ.get('MUSICGPT_DEFAULT_MODEL', 'v6-pro')
|
MUSICGPT_DEFAULT_MODEL = os.environ.get('MUSICGPT_DEFAULT_MODEL', 'v6-pro')
|
||||||
|
|
||||||
|
# Gokapi file-sharing server for stems uploads.
|
||||||
|
GOKAPI_URL = os.environ.get('GOKAPI_URL', '')
|
||||||
|
GOKAPI_API_KEY = os.environ.get('GOKAPI_API_KEY', '')
|
||||||
|
GOKAPI_EXPIRY_DAYS = int(os.environ.get('GOKAPI_EXPIRY_DAYS', '30'))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def musicgpt_webhook_base_url(cls):
|
def musicgpt_webhook_base_url(cls):
|
||||||
"""Return the public base URL used for MusicGPT webhooks."""
|
"""Return the public base URL used for MusicGPT webhooks."""
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@
|
||||||
# Required: APP_SECRET_KEY, ADMIN_PASSWORD, SMTP_PASS, PUBLIC_BASE_URL, MUSICGPT_API_KEY
|
# Required: APP_SECRET_KEY, ADMIN_PASSWORD, SMTP_PASS, PUBLIC_BASE_URL, MUSICGPT_API_KEY
|
||||||
# Optional: BOOTH_NAME, HOST_PORT, INTERNAL_PORT, PRICE_PER_VERSION,
|
# Optional: BOOTH_NAME, HOST_PORT, INTERNAL_PORT, PRICE_PER_VERSION,
|
||||||
# CURRENCY, MAX_REVISIONS, HERMES_API_KEY, DATABASE, UPLOAD_FOLDER,
|
# CURRENCY, MAX_REVISIONS, HERMES_API_KEY, DATABASE, UPLOAD_FOLDER,
|
||||||
# SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_FROM
|
# SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_FROM,
|
||||||
|
# GOKAPI_URL, GOKAPI_API_KEY, GOKAPI_EXPIRY_DAYS
|
||||||
#
|
#
|
||||||
# Named volumes keep the SQLite database and uploaded/downloaded songs persistent
|
# Named volumes keep the SQLite database and uploaded/downloaded songs persistent
|
||||||
# across container restarts and redeploys.
|
# across container restarts and redeploys.
|
||||||
|
|
@ -40,6 +41,9 @@ services:
|
||||||
- CURRENCY=${CURRENCY:-CAD}
|
- CURRENCY=${CURRENCY:-CAD}
|
||||||
- DATABASE=${DATABASE:-/app/data/booth.db}
|
- DATABASE=${DATABASE:-/app/data/booth.db}
|
||||||
- UPLOAD_FOLDER=${UPLOAD_FOLDER:-/app/uploads}
|
- UPLOAD_FOLDER=${UPLOAD_FOLDER:-/app/uploads}
|
||||||
|
- GOKAPI_URL=${GOKAPI_URL:-}
|
||||||
|
- GOKAPI_API_KEY=${GOKAPI_API_KEY:-}
|
||||||
|
- GOKAPI_EXPIRY_DAYS=${GOKAPI_EXPIRY_DAYS:-30}
|
||||||
ports:
|
ports:
|
||||||
- "${HOST_PORT:-0.0.0.0:8500}:${INTERNAL_PORT:-8000}"
|
- "${HOST_PORT:-0.0.0.0:8500}:${INTERNAL_PORT:-8000}"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
114
helpers.py
114
helpers.py
|
|
@ -34,6 +34,7 @@ from mutagen.easyid3 import EasyID3
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from models import update_request
|
from models import update_request
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -794,3 +795,116 @@ def get_musicgpt_cost_totals():
|
||||||
"total_cost": rows["total_cost"] or 0,
|
"total_cost": rows["total_cost"] or 0,
|
||||||
"stems_total": rows["stems_total"] or 0,
|
"stems_total": rows["stems_total"] or 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_stems_urls(stems_url_field):
|
||||||
|
"""
|
||||||
|
Parse the stems_url DB field into a list of (label, url) tuples.
|
||||||
|
The field can be:
|
||||||
|
- "label1: url1; label2: url2" (multiple individual stem files)
|
||||||
|
- A single bundle/zip URL
|
||||||
|
- A bare URL
|
||||||
|
"""
|
||||||
|
if not stems_url_field:
|
||||||
|
return []
|
||||||
|
urls = []
|
||||||
|
if ';' in stems_url_field or ': ' in stems_url_field:
|
||||||
|
parts = stems_url_field.split(';')
|
||||||
|
for part in parts:
|
||||||
|
part = part.strip()
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
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_field.strip()))
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_gokapi(file_bytes, filename, expiry_days=30):
|
||||||
|
"""
|
||||||
|
Upload a file to Gokapi and return the download URL.
|
||||||
|
Returns (download_url, error_message).
|
||||||
|
"""
|
||||||
|
gokapi_url = Config.GOKAPI_URL.rstrip('/')
|
||||||
|
api_key = Config.GOKAPI_API_KEY
|
||||||
|
if not gokapi_url or not api_key:
|
||||||
|
return None, 'Gokapi URL or API key not configured'
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{gokapi_url}/api/files/add",
|
||||||
|
headers={"apikey": api_key},
|
||||||
|
files={"file": (filename, file_bytes, "application/octet-stream")},
|
||||||
|
data={
|
||||||
|
"allowedDownloads": "0",
|
||||||
|
"expiryDays": str(expiry_days),
|
||||||
|
"password": "",
|
||||||
|
},
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
return None, f'Gokapi HTTP {resp.status_code}: {resp.text[:200]}'
|
||||||
|
data = resp.json()
|
||||||
|
if data.get('Result') != 'OK':
|
||||||
|
return None, f"Gokapi error: {data.get('Result')}"
|
||||||
|
file_info = data.get('FileInfo', {})
|
||||||
|
download_url = file_info.get('UrlDownload')
|
||||||
|
if not download_url:
|
||||||
|
return None, 'Gokapi returned no download URL'
|
||||||
|
return download_url, None
|
||||||
|
except Exception as e:
|
||||||
|
return None, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def process_stems_to_gokapi(req):
|
||||||
|
"""
|
||||||
|
Download stem files from MusicGPT CDN, zip them, upload the zip to Gokapi,
|
||||||
|
and return (gokapi_url, error_message).
|
||||||
|
|
||||||
|
Also updates the request row with the Gokapi download link in stems_link.
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
stems_url = req.get('stems_url') or ''
|
||||||
|
urls = parse_stems_urls(stems_url)
|
||||||
|
if not urls:
|
||||||
|
return None, 'No stems URL to process'
|
||||||
|
|
||||||
|
# If there's only one URL and it's already a .zip, upload it directly.
|
||||||
|
if len(urls) == 1 and urls[0][1].endswith('.zip'):
|
||||||
|
try:
|
||||||
|
r = requests.get(urls[0][1], timeout=120)
|
||||||
|
r.raise_for_status()
|
||||||
|
zip_bytes = r.content
|
||||||
|
except Exception as e:
|
||||||
|
return None, f'Failed to download stems zip: {e}'
|
||||||
|
else:
|
||||||
|
# Download each stem file and bundle into a zip in memory.
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for label, url in urls:
|
||||||
|
try:
|
||||||
|
r = requests.get(url, timeout=60)
|
||||||
|
r.raise_for_status()
|
||||||
|
filename = url.split('/')[-1] if '/' in url else f"{label}.mp3"
|
||||||
|
if '.' not in filename:
|
||||||
|
filename = f"{label}.mp3"
|
||||||
|
zf.writestr(filename, r.content)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
zip_bytes = buf.getvalue()
|
||||||
|
|
||||||
|
title = req.get('suno_title') or req.get('name') or f'request_{req["id"]}'
|
||||||
|
zip_filename = f"stems_{title}.zip"
|
||||||
|
|
||||||
|
download_url, error = upload_to_gokapi(zip_bytes, zip_filename, Config.GOKAPI_EXPIRY_DAYS)
|
||||||
|
if error:
|
||||||
|
return None, error
|
||||||
|
|
||||||
|
# Save the Gokapi link to stems_link in the database.
|
||||||
|
update_request(req['id'], stems_link=download_url)
|
||||||
|
return download_url, None
|
||||||
|
|
|
||||||
|
|
@ -626,7 +626,11 @@
|
||||||
{% if req.stems_error %}
|
{% if req.stems_error %}
|
||||||
<div class="flash error">{{ req.stems_error }}</div>
|
<div class="flash error">{{ req.stems_error }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if req.stems_url %}
|
{% if req.stems_link %}
|
||||||
|
<div class="actions">
|
||||||
|
<a href="{{ req.stems_link }}" 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>
|
||||||
|
</div>
|
||||||
|
{% elif req.stems_url %}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<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>
|
<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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue