Add MP3 metadata settings and auto-tag uploaded files with title, artist, album, year, genre, comment
This commit is contained in:
parent
98b191ff3c
commit
d74a715216
4 changed files with 130 additions and 30 deletions
126
app.py
126
app.py
|
|
@ -44,6 +44,10 @@ from config import Config
|
|||
from models import init_db, close_db, create_request, get_request_by_id, get_request_by_token, list_requests, update_request, now_utc, delete_request, reset_all_requests, get_db
|
||||
from models import SCHEMA
|
||||
|
||||
# Audio metadata import
|
||||
from mutagen.mp3 import MP3
|
||||
from mutagen.easyid3 import EasyID3
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# App setup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -98,13 +102,15 @@ def upload_path(request_id):
|
|||
return p
|
||||
|
||||
|
||||
def save_upload(request_id, file_obj, version):
|
||||
def save_upload(request_id, file_obj, version, song_title=None):
|
||||
"""
|
||||
Save an uploaded MP3 file for a request, preserving the original filename
|
||||
with a version prefix (e.g. A - MySong.mp3 / B - MySong.mp3).
|
||||
Applies the configured metadata tags (title, artist, album, year, genre, comment).
|
||||
:param request_id: database ID of the request
|
||||
:param file_obj: Flask FileStorage from request.files
|
||||
:param version: 'a' or 'b'
|
||||
:param song_title: title to write into the MP3 title tag
|
||||
:return: full filesystem path saved, or None on missing/invalid file
|
||||
"""
|
||||
if not file_obj or file_obj.filename == '':
|
||||
|
|
@ -121,9 +127,75 @@ def save_upload(request_id, file_obj, version):
|
|||
p = upload_path(request_id)
|
||||
dest = p / filename
|
||||
file_obj.save(dest)
|
||||
apply_mp3_tags(str(dest), song_title)
|
||||
return str(dest)
|
||||
|
||||
|
||||
def apply_mp3_tags(path, title=None):
|
||||
"""
|
||||
Write or overwrite common ID3 tags on an MP3 file using values from
|
||||
runtime booth settings. The saved Suno title is written to the Title tag.
|
||||
"""
|
||||
cfg = load_booth_settings()
|
||||
try:
|
||||
audio = MP3(path)
|
||||
# Ensure EasyID3 wrapper is available for the file.
|
||||
if audio.tags is None:
|
||||
audio.add_tags()
|
||||
if not isinstance(audio.tags, EasyID3):
|
||||
audio.tags = EasyID3()
|
||||
|
||||
tags = audio.tags
|
||||
if title:
|
||||
tags['title'] = title
|
||||
if cfg.get('artist'):
|
||||
tags['artist'] = cfg['artist']
|
||||
if cfg.get('album'):
|
||||
tags['album'] = cfg['album']
|
||||
if cfg.get('year'):
|
||||
tags['date'] = str(cfg['year'])
|
||||
if cfg.get('genre'):
|
||||
tags['genre'] = cfg['genre']
|
||||
|
||||
audio.save()
|
||||
|
||||
# Comment requires a real ID3 frame, not EasyID3.
|
||||
if cfg.get('comment'):
|
||||
from mutagen.id3 import ID3, COMM
|
||||
audio2 = MP3(path)
|
||||
if audio2.tags is None:
|
||||
audio2.add_tags()
|
||||
audio2.tags["COMM"] = COMM(encoding=3, lang='eng', desc='Comment', text=cfg['comment'])
|
||||
audio2.save()
|
||||
except Exception as e:
|
||||
# Don't fail the upload just because tagging failed; log/flash only if inside request context.
|
||||
try:
|
||||
flash(f'Warning: could not tag MP3: {e}', 'error')
|
||||
except RuntimeError:
|
||||
import logging
|
||||
logging.getLogger('app').warning('Could not tag MP3 %s: %s', path, e)
|
||||
|
||||
|
||||
def load_booth_settings():
|
||||
"""Load persistent runtime settings from JSON file inside the upload parent."""
|
||||
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||
if cfg_path.exists():
|
||||
try:
|
||||
return json.loads(cfg_path.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def save_booth_settings(settings):
|
||||
"""Persist runtime settings to JSON file."""
|
||||
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||
try:
|
||||
cfg_path.write_text(json.dumps(settings, indent=2))
|
||||
except OSError as e:
|
||||
flash(f'Warning: could not save settings: {e}', 'error')
|
||||
|
||||
|
||||
def send_email(to, subject, body, attachments=None):
|
||||
"""
|
||||
Send an email via SMTP_SSL.
|
||||
|
|
@ -206,13 +278,7 @@ def play(token):
|
|||
abort(404)
|
||||
|
||||
# Load runtime max revisions setting.
|
||||
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||
runtime_settings = {}
|
||||
if cfg_path.exists():
|
||||
try:
|
||||
runtime_settings = json.loads(cfg_path.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
runtime_settings = {}
|
||||
runtime_settings = load_booth_settings()
|
||||
max_revisions = runtime_settings.get('max_revisions', current_app.config['MAX_REVISIONS'])
|
||||
revisions_left = max(0, max_revisions - (req.get('revision_count') or 0))
|
||||
|
||||
|
|
@ -255,13 +321,7 @@ def revise(token):
|
|||
abort(404)
|
||||
|
||||
# Enforce max revisions limit for customer-submitted revisions.
|
||||
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||
runtime_settings = {}
|
||||
if cfg_path.exists():
|
||||
try:
|
||||
runtime_settings = json.loads(cfg_path.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
runtime_settings = {}
|
||||
runtime_settings = load_booth_settings()
|
||||
max_revisions = runtime_settings.get('max_revisions', current_app.config['MAX_REVISIONS'])
|
||||
current_count = req.get('revision_count') or 0
|
||||
if current_count >= max_revisions:
|
||||
|
|
@ -394,8 +454,10 @@ def admin_request(rid):
|
|||
|
||||
elif action == 'upload_songs':
|
||||
# Save uploaded MP3 files for Version A and/or Version B.
|
||||
a_path = save_upload(rid, request.files.get('song_a'), 'a')
|
||||
b_path = save_upload(rid, request.files.get('song_b'), 'b')
|
||||
# Use the saved Suno title as the MP3 title tag if available.
|
||||
song_title = req.get('suno_title') or None
|
||||
a_path = save_upload(rid, request.files.get('song_a'), 'a', song_title)
|
||||
b_path = save_upload(rid, request.files.get('song_b'), 'b', song_title)
|
||||
fields = {}
|
||||
if a_path:
|
||||
fields['song_a_path'] = a_path
|
||||
|
|
@ -508,12 +570,7 @@ def admin_settings():
|
|||
|
||||
# Load persistent runtime settings (max_revisions overrides env var if set).
|
||||
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||
runtime_settings = {}
|
||||
if cfg_path.exists():
|
||||
try:
|
||||
runtime_settings = json.loads(cfg_path.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
runtime_settings = {}
|
||||
runtime_settings = load_booth_settings()
|
||||
current_max_revisions = runtime_settings.get('max_revisions', current_app.config['MAX_REVISIONS'])
|
||||
|
||||
# Compute upload folder stats.
|
||||
|
|
@ -598,18 +655,26 @@ def admin_settings():
|
|||
val = int(request.form.get('max_revisions', '2').strip())
|
||||
if val < 0:
|
||||
raise ValueError
|
||||
# Store in a simple config file so it persists across restarts.
|
||||
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||
data = {}
|
||||
if cfg_path.exists():
|
||||
data = json.loads(cfg_path.read_text())
|
||||
data['max_revisions'] = val
|
||||
cfg_path.write_text(json.dumps(data))
|
||||
cfg = load_booth_settings()
|
||||
cfg['max_revisions'] = val
|
||||
save_booth_settings(cfg)
|
||||
flash(f'Maximum revisions set to {val}.', 'success')
|
||||
except ValueError:
|
||||
flash('Invalid revision limit. Please enter a non-negative number.', 'error')
|
||||
return redirect(url_for('admin_settings'))
|
||||
|
||||
elif action == 'save_metadata':
|
||||
# Update MP3 metadata defaults from the settings form.
|
||||
cfg = load_booth_settings()
|
||||
cfg['artist'] = request.form.get('artist', '').strip() or None
|
||||
cfg['album'] = request.form.get('album', '').strip() or None
|
||||
cfg['year'] = request.form.get('year', '').strip() or None
|
||||
cfg['genre'] = request.form.get('genre', '').strip() or None
|
||||
cfg['comment'] = request.form.get('comment', '').strip() or None
|
||||
save_booth_settings(cfg)
|
||||
flash('MP3 metadata defaults saved.', 'success')
|
||||
return redirect(url_for('admin_settings'))
|
||||
|
||||
return render_template(
|
||||
'admin/settings.html',
|
||||
health=health,
|
||||
|
|
@ -623,6 +688,7 @@ def admin_settings():
|
|||
db_path=str(db_path),
|
||||
upload_path=str(upload_root),
|
||||
current_max_revisions=current_max_revisions,
|
||||
metadata=runtime_settings,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
7
booth_settings.json
Normal file
7
booth_settings.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"artist": "Trollgorithm",
|
||||
"album": "Theme Booth 2026",
|
||||
"year": "2026",
|
||||
"genre": "Country Metal",
|
||||
"comment": "Custom booth song"
|
||||
}
|
||||
|
|
@ -12,3 +12,4 @@ flask
|
|||
gunicorn
|
||||
python-dotenv
|
||||
werkzeug
|
||||
mutagen
|
||||
|
|
|
|||
|
|
@ -206,6 +206,32 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!-- MP3 metadata defaults -->
|
||||
<div class="section">
|
||||
<h2>MP3 Metadata Tags</h2>
|
||||
<p class="copy-hint">These values are written into every uploaded MP3 file. The song title from the prompt is written to the Title tag automatically.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_metadata">
|
||||
|
||||
<label for="artist">Artist</label>
|
||||
<input type="text" id="artist" name="artist" value="{{ metadata.get('artist', '') }}" placeholder="e.g. Trollgorithm">
|
||||
|
||||
<label for="album">Album</label>
|
||||
<input type="text" id="album" name="album" value="{{ metadata.get('album', '') }}" placeholder="e.g. Theme Song Booth 2026">
|
||||
|
||||
<label for="year">Year</label>
|
||||
<input type="text" id="year" name="year" value="{{ metadata.get('year', '') }}" placeholder="e.g. 2026">
|
||||
|
||||
<label for="genre">Genre</label>
|
||||
<input type="text" id="genre" name="genre" value="{{ metadata.get('genre', '') }}" placeholder="e.g. Country Metal">
|
||||
|
||||
<label for="comment">Comment</label>
|
||||
<textarea id="comment" name="comment" rows="3" placeholder="e.g. Custom theme song generated at the booth">{{ metadata.get('comment', '') }}</textarea>
|
||||
|
||||
<button type="submit">Save Metadata Defaults</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- System reset -->
|
||||
<div class="danger-zone">
|
||||
<h2>⚠️ Reset System</h2>
|
||||
|
|
|
|||
Reference in a new issue