Group genre dropdowns into non-selectable optgroup sections
- Load grouped genre list (music_genres_grouped.txt) with section headings - Render headings as <optgroup> labels (visible, not selectable) - Add Disney under CHILDREN'S & HOLIDAY (was missing from grouped file) - Bump version to 0.9.3
This commit is contained in:
parent
07e92a522a
commit
868b59077f
7 changed files with 360 additions and 21 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ The MusicGPT fork automates song generation: instead of manually creating songs
|
|||
|
||||
| Page | Path | Purpose |
|
||||
|------|------|---------|
|
||||
| Request form | `/request` | Visitors enter email, name, pronouns, hobbies, notable facts, and extra requests. The music style is picked from three dropdowns: **Decade** (required), **Basic style** (required), and **Additional style** (optional). Decades and genres are loaded from `/mnt/Storage/Decades.txt` and `/mnt/Storage/Music Genres.txt`. Rate limited to 5 submissions per minute per IP. |
|
||||
| Request form | `/request` | Visitors enter email, name, pronouns, hobbies, notable facts, and extra requests. The music style is picked from three dropdowns: **Decade** (required), **Basic style** (required), and **Additional style** (optional). Decades and genres are loaded from `/mnt/Storage/Decades.txt` and `/mnt/Storage/music_genres_grouped.txt` (a grouped file where section headings are rendered as non-selectable dropdown groups). Rate limited to 5 submissions per minute per IP. |
|
||||
| Closed page | `/request` (when booth is closed) | Shows a friendly closed banner instead of the form when the operator marks the booth closed. |
|
||||
| Thanks | `/thanks/<id>` | Confirmation page shown after a request is submitted. |
|
||||
| Order status | `/status` | Customers enter their email to see all their requests and statuses. |
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.9.2
|
||||
0.9.3
|
||||
|
|
|
|||
14
app.py
14
app.py
|
|
@ -60,7 +60,7 @@ from models import SCHEMA, get_requests_by_email, log_revision, list_revision_hi
|
|||
|
||||
# Helpers (extracted from app.py to keep the route file manageable)
|
||||
from helpers import (
|
||||
MUSIC_GENRES, DECADES, parse_style_genre, build_style_genre,
|
||||
GENRE_GROUPS, DECADES, parse_style_genre, build_style_genre,
|
||||
is_admin, require_admin, admin_password_ok, is_valid_email,
|
||||
allowed_file, upload_path, save_upload,
|
||||
apply_mp3_tags,
|
||||
|
|
@ -186,13 +186,13 @@ def request_form():
|
|||
pronouns = request.form.get('pronouns', '').strip()
|
||||
if not pronouns:
|
||||
flash('Please select your pronouns.', 'error')
|
||||
return render_template('request.html', form=request.form, decades=DECADES, genres=MUSIC_GENRES), 400
|
||||
return render_template('request.html', form=request.form, decades=DECADES, genre_groups=GENRE_GROUPS), 400
|
||||
if not decade:
|
||||
flash('Please select a decade.', 'error')
|
||||
return render_template('request.html', form=request.form, decades=DECADES, genres=MUSIC_GENRES), 400
|
||||
return render_template('request.html', form=request.form, decades=DECADES, genre_groups=GENRE_GROUPS), 400
|
||||
if not basic_style:
|
||||
flash('Please select a basic style.', 'error')
|
||||
return render_template('request.html', form=request.form, decades=DECADES, genres=MUSIC_GENRES), 400
|
||||
return render_template('request.html', form=request.form, decades=DECADES, genre_groups=GENRE_GROUPS), 400
|
||||
form_data = {
|
||||
'name': request.form.get('name', '').strip(),
|
||||
'email': request.form.get('email', '').strip().lower(),
|
||||
|
|
@ -206,7 +206,7 @@ def request_form():
|
|||
}
|
||||
if not is_valid_email(form_data['email']):
|
||||
flash('Please enter a valid email address.', 'error')
|
||||
return render_template('request.html', form=form_data, decades=DECADES, genres=MUSIC_GENRES), 400
|
||||
return render_template('request.html', form=form_data, decades=DECADES, genre_groups=GENRE_GROUPS), 400
|
||||
rid = create_request(**form_data)
|
||||
|
||||
# Notify operator via ntfy when a new request comes in.
|
||||
|
|
@ -251,7 +251,7 @@ def request_form():
|
|||
|
||||
flash('Your request has been submitted! Check your email soon.', 'success')
|
||||
return redirect(url_for('thanks', rid=rid))
|
||||
return render_template('request.html', form=None, decades=DECADES, genres=MUSIC_GENRES)
|
||||
return render_template('request.html', form=None, decades=DECADES, genre_groups=GENRE_GROUPS)
|
||||
|
||||
|
||||
@app.route('/api/ping', methods=['GET'])
|
||||
|
|
@ -1521,7 +1521,7 @@ def admin_request(rid):
|
|||
req=req,
|
||||
style_parts=parse_style_genre(req.get('style_genre')),
|
||||
decades=DECADES,
|
||||
genres=MUSIC_GENRES,
|
||||
genre_groups=GENRE_GROUPS,
|
||||
statuses=STATUS_LABELS,
|
||||
file_exists=file_exists,
|
||||
basename=basename,
|
||||
|
|
|
|||
35
helpers.py
35
helpers.py
|
|
@ -50,9 +50,9 @@ from config import Config
|
|||
# Genre / decade helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_GENRES_PATH = Path('/mnt/Storage/Music Genres.txt')
|
||||
_GENRES_PATH = Path('/mnt/Storage/music_genres_grouped.txt')
|
||||
_DECADES_PATH = Path('/mnt/Storage/Decades.txt')
|
||||
_FALLBACK_GENRES_PATH = Path(__file__).parent / 'lists' / 'music_genres.txt'
|
||||
_FALLBACK_GENRES_PATH = Path(__file__).parent / 'lists' / 'music_genres_grouped.txt'
|
||||
_FALLBACK_DECADES_PATH = Path(__file__).parent / 'lists' / 'decades.txt'
|
||||
|
||||
|
||||
|
|
@ -72,9 +72,38 @@ def _load_list(primary: Path, fallback: Path) -> list[str]:
|
|||
return _load_lines(fallback)
|
||||
|
||||
|
||||
MUSIC_GENRES = _load_list(_GENRES_PATH, _FALLBACK_GENRES_PATH)
|
||||
def _load_genre_groups(primary: Path, fallback: Path) -> list[dict]:
|
||||
"""Parse a grouped genre file into [{'label': str, 'genres': [str, ...]}, ...].
|
||||
|
||||
A blank line ends the current group; the first line after a blank line (or
|
||||
the very first line) is the group heading, and every following line until
|
||||
the next blank line is a genre under that heading. Headings are rendered as
|
||||
non-selectable <optgroup> labels, so they are visible but not clickable.
|
||||
"""
|
||||
path = primary if primary.exists() else fallback
|
||||
if not path.exists():
|
||||
return []
|
||||
groups = []
|
||||
current = None
|
||||
for raw in path.read_text(encoding='utf-8').splitlines():
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
current = None
|
||||
continue
|
||||
if current is None:
|
||||
current = {'label': line, 'genres': []}
|
||||
groups.append(current)
|
||||
else:
|
||||
current['genres'].append(line)
|
||||
return groups
|
||||
|
||||
|
||||
GENRE_GROUPS = _load_genre_groups(_GENRES_PATH, _FALLBACK_GENRES_PATH)
|
||||
DECADES = _load_list(_DECADES_PATH, _FALLBACK_DECADES_PATH)
|
||||
|
||||
# Flat list of every genre, derived from the grouped list so the two stay in sync.
|
||||
MUSIC_GENRES = [g for group in GENRE_GROUPS for g in group['genres']]
|
||||
|
||||
|
||||
def parse_style_genre(style_genre: str | None) -> dict:
|
||||
"""
|
||||
|
|
|
|||
294
lists/music_genres_grouped.txt
Executable file
294
lists/music_genres_grouped.txt
Executable file
|
|
@ -0,0 +1,294 @@
|
|||
ROCK
|
||||
Alternative Rock
|
||||
Art Rock
|
||||
Blues Rock
|
||||
Britpop
|
||||
Classic Rock
|
||||
Folk Rock
|
||||
Garage Rock
|
||||
Glam Rock
|
||||
Gothic Rock
|
||||
Grunge
|
||||
Hard Rock
|
||||
Indie
|
||||
Indie Rock
|
||||
J-Rock
|
||||
Krautrock
|
||||
Math Rock
|
||||
Neue Deutsche Welle
|
||||
New Romantic
|
||||
New Wave
|
||||
Post-Rock
|
||||
Power Pop
|
||||
Progressive Rock
|
||||
Psychedelic Rock
|
||||
Rock
|
||||
Rock & Roll
|
||||
Rockabilly
|
||||
Shoegaze
|
||||
Soft Rock
|
||||
Southern Rock
|
||||
Space Rock
|
||||
Stoner Rock
|
||||
Surf Rock
|
||||
Symphonic Rock
|
||||
Trop Rock
|
||||
Yacht Rock
|
||||
|
||||
METAL
|
||||
Black Metal
|
||||
Death Metal
|
||||
Deathcore
|
||||
Doom Metal
|
||||
Folk Metal
|
||||
Glam Metal
|
||||
Gothic Metal
|
||||
Heavy Metal
|
||||
Industrial Metal
|
||||
Metal
|
||||
Metalcore
|
||||
Nu Metal
|
||||
Power Metal
|
||||
Progressive Metal
|
||||
Symphonic Metal
|
||||
Thrash Metal
|
||||
|
||||
PUNK & EMO
|
||||
Afropunk
|
||||
Emo
|
||||
Hardcore Punk
|
||||
Midwest Emo
|
||||
Pop Punk
|
||||
Post-Hardcore
|
||||
Post-Punk
|
||||
Punk Rock
|
||||
Screamo
|
||||
Ska Punk
|
||||
|
||||
POP
|
||||
Adult Contemporary
|
||||
Art Pop
|
||||
Ballad
|
||||
Baroque Pop
|
||||
Bedroom Pop
|
||||
Bubblegum Pop
|
||||
C-Pop
|
||||
Cantopop
|
||||
City Pop
|
||||
Dance Pop
|
||||
Dream Pop
|
||||
Electropop
|
||||
Folk Pop
|
||||
Indie Pop
|
||||
J-Pop
|
||||
K-Pop
|
||||
Latin Pop
|
||||
Piano Ballad
|
||||
Pop
|
||||
Pop Rock
|
||||
Power Ballad
|
||||
Synthpop
|
||||
|
||||
HIP-HOP & RAP
|
||||
Boom Bap
|
||||
Cloud Rap
|
||||
Conscious Hip-Hop
|
||||
Crunk
|
||||
Drill
|
||||
G-Funk
|
||||
Gangsta Rap
|
||||
Grime
|
||||
Hip-Hop
|
||||
Latin Trap
|
||||
Lo-Fi Hip-Hop
|
||||
Miami Bass
|
||||
Phonk
|
||||
Trap
|
||||
UK Drill
|
||||
|
||||
R&B & SOUL
|
||||
Alternative R&B
|
||||
Doo-Wop
|
||||
Funk
|
||||
Motown
|
||||
Neo-Soul
|
||||
New Jack Swing
|
||||
R&B
|
||||
Slow Jam
|
||||
Soul
|
||||
|
||||
ELECTRONIC & DANCE
|
||||
Acid House
|
||||
Afro House
|
||||
Amapiano
|
||||
Ambient
|
||||
Bass Music
|
||||
Big Beat
|
||||
Big Room House
|
||||
Breakbeat
|
||||
Chicago House
|
||||
Chillout
|
||||
Chillwave
|
||||
Chiptune
|
||||
Dance
|
||||
Dark Ambient
|
||||
Darkwave
|
||||
Deep House
|
||||
Detroit Techno
|
||||
Disco
|
||||
Downtempo
|
||||
Drum & Bass
|
||||
Dubstep
|
||||
EBM
|
||||
Electro
|
||||
Electro House
|
||||
Electroclash
|
||||
Electronic
|
||||
Eurobeat
|
||||
Eurodance
|
||||
Future Bass
|
||||
Future Funk
|
||||
Goa Trance
|
||||
Happy Hardcore
|
||||
Hardstyle
|
||||
House
|
||||
Hyperpop
|
||||
IDM
|
||||
Industrial
|
||||
Italo Disco
|
||||
Jungle
|
||||
Lo-Fi
|
||||
Melodic Techno
|
||||
Minimal Techno
|
||||
Moombahton
|
||||
New Age
|
||||
Nu-Disco
|
||||
Progressive House
|
||||
Progressive Trance
|
||||
Psybient
|
||||
Psytrance
|
||||
Synthwave
|
||||
Tech House
|
||||
Techno
|
||||
Trance
|
||||
Trip-Hop
|
||||
Tropical House
|
||||
UK Garage
|
||||
Uplifting Trance
|
||||
Vaporwave
|
||||
|
||||
JAZZ
|
||||
Acid Jazz
|
||||
Bebop
|
||||
Big Band
|
||||
Cool Jazz
|
||||
Dixieland
|
||||
Free Jazz
|
||||
Jam Band
|
||||
Jazz
|
||||
Jazz Funk
|
||||
Jazz Fusion
|
||||
Latin Jazz
|
||||
Ragtime
|
||||
Smooth Jazz
|
||||
Swing
|
||||
|
||||
BLUES
|
||||
Blues
|
||||
|
||||
FOLK, COUNTRY & AMERICANA
|
||||
Acoustic
|
||||
Alt-Country
|
||||
Americana
|
||||
Bluegrass
|
||||
Celtic
|
||||
Country
|
||||
Country Pop
|
||||
Country Rock
|
||||
Folk
|
||||
Honky Tonk
|
||||
Indie Folk
|
||||
Irish Folk
|
||||
Outlaw Country
|
||||
Sea Shanty
|
||||
Singer-Songwriter
|
||||
Western Swing
|
||||
Zydeco
|
||||
|
||||
WORLD & REGIONAL
|
||||
Afrobeat
|
||||
Afrobeats
|
||||
Bhangra
|
||||
Bollywood
|
||||
Highlife
|
||||
Klezmer
|
||||
Native American
|
||||
Polka
|
||||
Qawwali
|
||||
World Music
|
||||
|
||||
LATIN
|
||||
Bachata
|
||||
Bossa Nova
|
||||
Corrido
|
||||
Cumbia
|
||||
Flamenco
|
||||
Latin
|
||||
Mariachi
|
||||
Merengue
|
||||
Norteño
|
||||
Reggaeton
|
||||
Regional Mexican
|
||||
Salsa
|
||||
Samba
|
||||
Tango
|
||||
Tejano
|
||||
|
||||
REGGAE & CARIBBEAN
|
||||
Calypso
|
||||
Dancehall
|
||||
Dub
|
||||
Reggae
|
||||
Roots Reggae
|
||||
Ska
|
||||
Soca
|
||||
|
||||
CLASSICAL & ORCHESTRAL
|
||||
Baroque
|
||||
Chamber Music
|
||||
Classical
|
||||
Neoclassical
|
||||
Opera
|
||||
Orchestral
|
||||
|
||||
CHRISTIAN & GOSPEL
|
||||
Christian Hip-Hop
|
||||
Christian Rock
|
||||
Contemporary Christian
|
||||
Gospel
|
||||
Worship
|
||||
|
||||
SOUNDTRACK, CINEMATIC & THEATRE
|
||||
Anime
|
||||
Cabaret
|
||||
Cinematic
|
||||
Musical Theatre
|
||||
Soundtrack
|
||||
|
||||
CHILDREN'S & HOLIDAY
|
||||
Children's Music
|
||||
Christmas
|
||||
Disney
|
||||
Lullaby
|
||||
|
||||
EASY LISTENING & TRADITIONAL VOCAL
|
||||
A Cappella
|
||||
Barbershop
|
||||
Chanson
|
||||
Easy Listening
|
||||
Lounge
|
||||
|
||||
OTHER / EXPERIMENTAL
|
||||
Avant-Garde
|
||||
Experimental
|
||||
Noise
|
||||
|
|
@ -133,17 +133,25 @@
|
|||
<label for="customer_style_genre_basic">Basic style <span class="required">*</span></label>
|
||||
<select id="customer_style_genre_basic" name="basic_style" required>
|
||||
<option value="" {% if not style_parts.basic_style %}selected{% endif %}>— Select style —</option>
|
||||
{% for g in genres %}
|
||||
{% for group in genre_groups %}
|
||||
<optgroup label="{{ group.label }}">
|
||||
{% for g in group.genres %}
|
||||
<option value="{{ g }}" {% if style_parts.basic_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="customer_style_genre_additional">Additional style (optional)</label>
|
||||
<select id="customer_style_genre_additional" name="additional_style">
|
||||
<option value="" {% if not style_parts.additional_style %}selected{% endif %}>— None —</option>
|
||||
{% for g in genres %}
|
||||
{% for group in genre_groups %}
|
||||
<optgroup label="{{ group.label }}">
|
||||
{% for g in group.genres %}
|
||||
<option value="{{ g }}" {% if style_parts.additional_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<p class="copy-hint">Stored style: {{ req.style_genre or '—' }}</p>
|
||||
|
|
|
|||
|
|
@ -62,17 +62,25 @@
|
|||
<label for="basic_style">Basic style <span class="required">*</span></label>
|
||||
<select id="basic_style" name="basic_style" required>
|
||||
<option value="" {% if not form or not form.basic_style %}selected{% endif %}>— Select style —</option>
|
||||
{% for g in genres %}
|
||||
{% for group in genre_groups %}
|
||||
<optgroup label="{{ group.label }}">
|
||||
{% for g in group.genres %}
|
||||
<option value="{{ g }}" {% if form and form.basic_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="additional_style">Additional style (optional)</label>
|
||||
<select id="additional_style" name="additional_style">
|
||||
<option value="" {% if not form or not form.additional_style %}selected{% endif %}>— None —</option>
|
||||
{% for g in genres %}
|
||||
{% for group in genre_groups %}
|
||||
<optgroup label="{{ group.label }}">
|
||||
{% for g in group.genres %}
|
||||
<option value="{{ g }}" {% if form and form.additional_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<p class="copy-hint">Selected style: <span id="style-preview">{% if form %}{{ form.decade or '' }}{% if form.decade and form.basic_style %}, {% endif %}{{ form.basic_style or '' }}{% if form.basic_style and form.additional_style %}, {% endif %}{{ form.additional_style or '' }}{% else %}—{% endif %}</span></p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue