""" 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, and player token. - Indexes on status and player_token for fast queue/lookup. """ import sqlite3 import secrets from datetime import datetime, timezone 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, 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, player_token TEXT NOT NULL UNIQUE, revision_count INTEGER DEFAULT 0, revision_note TEXT ); CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(status); CREATE INDEX IF NOT EXISTS idx_requests_token ON requests(player_token); """ 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. Safe to run multiple times.""" db = sqlite3.connect(current_app.config['DATABASE']) db.executescript(SCHEMA) 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): """ 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, extra_requests, vocal_gender, player_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", (name, email, hobbies, notable_facts, style_genre, extra_requests, vocal_gender, new_token()) ) 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.""" db = get_db() if status: 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 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 by id. Does NOT delete associated files.""" db = get_db() db.execute('DELETE FROM requests WHERE id = ?', (request_id,)) db.commit() def reset_all_requests(): """Delete every row in the requests table. Does NOT delete files.""" db = get_db() db.execute('DELETE FROM requests') db.commit()