Initial fork from theme-song-booth
This commit is contained in:
commit
99855b1212
33 changed files with 5723 additions and 0 deletions
257
models.py
Normal file
257
models.py
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
"""
|
||||
models.py
|
||||
=========
|
||||
SQLite database layer for the Theme Song Booth.
|
||||
|
||||
This module defines the schema and all database operations. Flask's
|
||||
application context (`g`) is used to manage one connection per request.
|
||||
|
||||
Schema overview (see SCHEMA constant):
|
||||
- requests table stores customer data, generated prompts, file paths,
|
||||
approval state, email timestamps, payment reference, player token,
|
||||
vocal gender preference, revision count, revision note, and operator notes.
|
||||
- Revisions: when a customer requests changes, the current A/B MP3 files are
|
||||
renamed to archived "RevN-" copies and new versions are uploaded later.
|
||||
- operator_notes is an internal column for the booth team and is never shown
|
||||
to customers.
|
||||
- Indexes on status and player_token for fast queue/lookup.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from flask import current_app, g
|
||||
|
||||
# SQL executed by init_db() to create the requests table and indexes.
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
hobbies TEXT,
|
||||
notable_facts TEXT,
|
||||
style_genre TEXT,
|
||||
pronouns TEXT,
|
||||
extra_requests TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
suno_title TEXT,
|
||||
suno_style TEXT,
|
||||
suno_lyrics TEXT,
|
||||
song_a_path TEXT,
|
||||
song_b_path TEXT,
|
||||
vocal_gender TEXT,
|
||||
customer_approved TEXT DEFAULT 'none',
|
||||
approval_notified_at TIMESTAMP,
|
||||
preview_sent_at TIMESTAMP,
|
||||
delivery_sent_at TIMESTAMP,
|
||||
square_payment_ref TEXT,
|
||||
admin_alert_email TEXT, -- reserved for future operator alerts; currently unused
|
||||
player_token TEXT NOT NULL UNIQUE,
|
||||
revision_count INTEGER DEFAULT 0,
|
||||
revision_note TEXT,
|
||||
operator_notes TEXT,
|
||||
stems_link TEXT,
|
||||
stems_interest INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_requests_token ON requests(player_token);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS revision_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
request_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
revision_count INTEGER NOT NULL,
|
||||
note TEXT,
|
||||
old_song_a_path TEXT,
|
||||
old_song_b_path TEXT,
|
||||
new_song_a_path TEXT,
|
||||
new_song_b_path TEXT,
|
||||
FOREIGN KEY (request_id) REFERENCES requests(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_revision_history_request ON revision_history(request_id);
|
||||
"""
|
||||
|
||||
|
||||
def get_db():
|
||||
"""Get or create a SQLite connection tied to the current Flask request context."""
|
||||
if 'db' not in g:
|
||||
g.db = sqlite3.connect(current_app.config['DATABASE'])
|
||||
g.db.row_factory = sqlite3.Row
|
||||
return g.db
|
||||
|
||||
|
||||
def close_db(e=None):
|
||||
"""Close the request-scoped SQLite connection. Registered as teardown handler."""
|
||||
db = g.pop('db', None)
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Create the database file and tables, adding any missing columns to existing tables."""
|
||||
db_path = current_app.config['DATABASE']
|
||||
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
db = sqlite3.connect(db_path)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.executescript(SCHEMA)
|
||||
|
||||
# SQLite ALTER TABLE is limited; add newer columns if they are missing.
|
||||
expected_columns = {
|
||||
'requests': [
|
||||
'id', 'created_at', 'name', 'email', 'hobbies', 'notable_facts',
|
||||
'style_genre', 'pronouns', 'extra_requests', 'status', 'suno_title', 'suno_style',
|
||||
'suno_lyrics', 'song_a_path', 'song_b_path', 'vocal_gender',
|
||||
'customer_approved', 'approval_notified_at', 'preview_sent_at',
|
||||
'delivery_sent_at', 'square_payment_ref', 'admin_alert_email',
|
||||
'player_token', 'revision_count', 'revision_note', 'operator_notes',
|
||||
'stems_link', 'stems_interest'
|
||||
],
|
||||
'revision_history': [
|
||||
'id', 'created_at', 'request_id', 'revision_count', 'note',
|
||||
'old_song_a_path', 'old_song_b_path', 'new_song_a_path', 'new_song_b_path'
|
||||
]
|
||||
}
|
||||
for table, columns in expected_columns.items():
|
||||
existing = {r['name'] for r in db.execute(f"PRAGMA table_info({table})")}
|
||||
for col in columns:
|
||||
if col not in existing:
|
||||
# revision_count must be INTEGER so arithmetic in app.py works.
|
||||
col_type = 'INTEGER' if col == 'revision_count' else 'TEXT'
|
||||
db.execute(f'ALTER TABLE {table} ADD COLUMN {col} {col_type}')
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
|
||||
def new_token():
|
||||
"""Generate a URL-safe random token used for private player links."""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def now_utc():
|
||||
"""Return current UTC time as ISO-8601 string for timestamp columns."""
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def create_request(name, email, hobbies, notable_facts, style_genre, extra_requests, vocal_gender=None, pronouns=None, stems_interest=0):
|
||||
"""
|
||||
Insert a new customer request.
|
||||
Returns the auto-generated request id.
|
||||
"""
|
||||
db = get_db()
|
||||
cur = db.execute(
|
||||
"""INSERT INTO requests
|
||||
(name, email, hobbies, notable_facts, style_genre, pronouns, extra_requests, vocal_gender, player_token, stems_interest)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(name, email, hobbies, notable_facts, style_genre, pronouns, extra_requests, vocal_gender, new_token(), 1 if stems_interest else 0)
|
||||
)
|
||||
db.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def get_request_by_id(request_id):
|
||||
"""Fetch one request by numeric id. Returns dict or None."""
|
||||
db = get_db()
|
||||
row = db.execute('SELECT * FROM requests WHERE id = ?', (request_id,)).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def get_request_by_token(token):
|
||||
"""Fetch one request by its private player token. Returns dict or None."""
|
||||
db = get_db()
|
||||
row = db.execute('SELECT * FROM requests WHERE player_token = ?', (token,)).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def list_requests(status=None):
|
||||
"""List all requests, optionally filtered by status, newest first.
|
||||
Normalizes whitespace in the status parameter so URLs like "Needs Upload"
|
||||
match the stored value."""
|
||||
db = get_db()
|
||||
if status:
|
||||
# Translate human filter names to stored status values.
|
||||
status = status.lower().replace(' ', '_')
|
||||
rows = db.execute('SELECT * FROM requests WHERE status = ? ORDER BY created_at DESC', (status,)).fetchall()
|
||||
else:
|
||||
rows = db.execute('SELECT * FROM requests ORDER BY created_at DESC').fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_requests_by_email(email):
|
||||
"""Fetch all requests for a given email address, newest first.
|
||||
Email is compared case-insensitively and stripped of whitespace."""
|
||||
db = get_db()
|
||||
rows = db.execute(
|
||||
"SELECT * FROM requests WHERE LOWER(TRIM(email)) = LOWER(TRIM(?)) ORDER BY created_at DESC",
|
||||
(email,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def update_request(request_id, **fields):
|
||||
"""
|
||||
Update arbitrary columns for a request.
|
||||
Example: update_request(1, status='prompt_ready', suno_style='...')
|
||||
"""
|
||||
if not fields:
|
||||
return
|
||||
db = get_db()
|
||||
cols = ', '.join(f'{k} = ?' for k in fields)
|
||||
vals = list(fields.values()) + [request_id]
|
||||
db.execute(f'UPDATE requests SET {cols} WHERE id = ?', vals)
|
||||
db.commit()
|
||||
|
||||
|
||||
def delete_request(request_id):
|
||||
"""Delete a single request row by id. Does NOT delete associated files
|
||||
(the caller in app.py removes uploads before/after this call)."""
|
||||
db = get_db()
|
||||
db.execute('DELETE FROM requests WHERE id = ?', (request_id,))
|
||||
db.commit()
|
||||
|
||||
|
||||
def log_revision(request_id, revision_count, note, old_a=None, old_b=None, new_a=None, new_b=None):
|
||||
"""Record a revision event in the revision_history table, creating it if it is missing."""
|
||||
db = get_db()
|
||||
try:
|
||||
db.execute(
|
||||
"""INSERT INTO revision_history
|
||||
(request_id, revision_count, note, old_song_a_path, old_song_b_path, new_song_a_path, new_song_b_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||||
(request_id, revision_count, note, old_a, old_b, new_a, new_b)
|
||||
)
|
||||
db.commit()
|
||||
except sqlite3.OperationalError as e:
|
||||
if 'no such table' in str(e):
|
||||
# Schema drift: table missing. Run init_db to add tables/columns, then retry once.
|
||||
init_db()
|
||||
db.execute(
|
||||
"""INSERT INTO revision_history
|
||||
(request_id, revision_count, note, old_song_a_path, old_song_b_path, new_song_a_path, new_song_b_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||||
(request_id, revision_count, note, old_a, old_b, new_a, new_b)
|
||||
)
|
||||
db.commit()
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def list_revision_history(request_id):
|
||||
"""Return all revision history rows for a request, oldest first."""
|
||||
db = get_db()
|
||||
rows = db.execute(
|
||||
'SELECT * FROM revision_history WHERE request_id = ? ORDER BY created_at ASC',
|
||||
(request_id,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def reset_all_requests():
|
||||
"""Delete every row in the requests table and reset id auto-increment."""
|
||||
db = get_db()
|
||||
db.execute('DELETE FROM requests')
|
||||
db.execute('DELETE FROM sqlite_sequence WHERE name = ?', ('requests',))
|
||||
db.commit()
|
||||
Loading…
Add table
Add a link
Reference in a new issue