import Database from 'better-sqlite3'; import path from 'path'; import fs from 'fs'; const DB_PATH = process.env.DB_PATH || path.resolve(process.cwd(), '..', 'spotify.db'); class DBWrapper { constructor() { this.instance = null; } get db() { if (!this.instance) { this.instance = new Database(DB_PATH); this.instance.pragma('journal_mode = WAL'); this.instance.pragma('foreign_keys = ON'); } return this.instance; } initialize() { // Ensure file exists if (!fs.existsSync(DB_PATH)) { fs.writeFileSync(DB_PATH, ''); } const sql = ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, display_name TEXT, email TEXT, avatar_url TEXT, country TEXT, product TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, access_token TEXT, refresh_token TEXT, token_expires_at INTEGER, last_synced_at INTEGER ); CREATE TABLE IF NOT EXISTS friendships ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_a_id TEXT NOT NULL, user_b_id TEXT NOT NULL, created_at INTEGER NOT NULL, UNIQUE(user_a_id, user_b_id) ); CREATE TABLE IF NOT EXISTS friend_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, from_user_id TEXT NOT NULL, to_user_id TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('pending','accepted','declined')), created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, UNIQUE(from_user_id, to_user_id) ); CREATE TABLE IF NOT EXISTS recently_played ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, played_at INTEGER NOT NULL, track_json TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS top_tracks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, time_range TEXT NOT NULL, rank INTEGER NOT NULL, track_json TEXT NOT NULL, UNIQUE(user_id, time_range, rank) ); CREATE TABLE IF NOT EXISTS top_artists ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, time_range TEXT NOT NULL, rank INTEGER NOT NULL, artist_json TEXT NOT NULL, UNIQUE(user_id, time_range, rank) ); CREATE TABLE IF NOT EXISTS mixed_playlists ( id TEXT PRIMARY KEY, creator_id TEXT NOT NULL, partner_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT, vibe TEXT, genres TEXT, -- JSON array of genres track_uris TEXT, -- JSON array of track URIs creator_spotify_id TEXT, partner_spotify_id TEXT, creator_spotify_url TEXT, partner_spotify_url TEXT, creator_spotify_image_url TEXT, partner_spotify_image_url TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY (creator_id) REFERENCES users(id), FOREIGN KEY (partner_id) REFERENCES users(id) ); `; this.db.exec(sql); // Migrations for existing DBs try { const cols = this.db.prepare("PRAGMA table_info(users)").all(); const hasLastSynced = cols.some(c => c.name === 'last_synced_at'); if (!hasLastSynced) { this.db.exec('ALTER TABLE users ADD COLUMN last_synced_at INTEGER'); } } catch { } // Migration for mixed_playlists table try { const playlistCols = this.db.prepare("PRAGMA table_info(mixed_playlists)").all(); const hasCreatorImageUrl = playlistCols.some(c => c.name === 'creator_spotify_image_url'); const hasPartnerImageUrl = playlistCols.some(c => c.name === 'partner_spotify_image_url'); if (!hasCreatorImageUrl) { this.db.exec('ALTER TABLE mixed_playlists ADD COLUMN creator_spotify_image_url TEXT'); } if (!hasPartnerImageUrl) { this.db.exec('ALTER TABLE mixed_playlists ADD COLUMN partner_spotify_image_url TEXT'); } } catch { } } } export const db = new DBWrapper();