create table users ( id bigserial primary key, name text not null, email text not null unique, password_hash text not null, avatar text, banned boolean not null default false, created_at timestamptz not null default now() ); create table sessions ( token text primary key, user_id bigint not null references users (id) on delete cascade, idle_ttl interval not null, expires_at timestamptz not null, created_at timestamptz not null default now() ); create index on sessions (user_id); create table invites ( id bigserial primary key, code text not null unique, is_valid boolean not null default true, created_at timestamptz not null default now() ); create table songs ( id bigserial primary key, title text not null, artist text not null, genre text not null, description text, -- LRC or plain text, told apart by whether the first line starts with '['. Not covered by the -- lock: nobody reviewed the lyrics. lyrics text, audio_file text not null, duration_seconds integer not null, source_url text, submitted_by bigint not null references users (id), created_at timestamptz not null default now() ); create index on songs (created_at desc); create table submissions ( id bigserial primary key, user_id bigint not null references users (id) on delete cascade, status text not null default 'queued', status_msg text, source_url text, tmp_path text, title text, artist text, genre text, description text, lyrics text, created_at timestamptz not null default now(), constraint submissions_status check ( status in ('queued', 'downloading', 'converting', 'ready', 'failed') ) ); -- The submission quota (5 per rolling 24h, failures excluded) reads this. create index on submissions (user_id, created_at desc); create table reviews ( id bigserial primary key, song_id bigint not null references songs (id) on delete cascade, reviewer_id bigint not null references users (id), score integer not null check (score between 1 and 100), text text not null, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (song_id, reviewer_id) ); create index on reviews (song_id); -- The queue asks "songs this member has not reviewed" — that lookup is by reviewer. create index on reviews (reviewer_id, song_id); create table reports ( id bigserial primary key, user_id bigint not null references users (id) on delete cascade, body text not null, page text, user_agent text, resolved_at timestamptz, created_at timestamptz not null default now() );