diff --git a/app.py b/app.py index 7bcfc66..2168a62 100644 --- a/app.py +++ b/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, ) diff --git a/booth_settings.json b/booth_settings.json new file mode 100644 index 0000000..7a60bf8 --- /dev/null +++ b/booth_settings.json @@ -0,0 +1,7 @@ +{ + "artist": "Trollgorithm", + "album": "Theme Booth 2026", + "year": "2026", + "genre": "Country Metal", + "comment": "Custom booth song" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 67ed02d..cbfdbf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ flask gunicorn python-dotenv werkzeug +mutagen diff --git a/templates/admin/settings.html b/templates/admin/settings.html index d7f3169..0122539 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -206,6 +206,32 @@ + +
These values are written into every uploaded MP3 file. The song title from the prompt is written to the Title tag automatically.
+ +