Documentation/Source example
Example source

samples/basic/SQLite Playback History.js

Referenced by:utils.OpenDatabase
window.DefineScript('SQLite Playback History');
include('docs/Flags.js');
include('docs/Helpers.js');

// A small persistent playback-history panel.
// The database is stored in the foobar2000 profile folder and survives restarts.
// It demonstrates schema creation, parameters, a reusable prepared statement,
// aggregate queries, SQLite exception handling and explicit cleanup.

const DB_PATH = fb.ProfilePath + 'jsplitter-playback-history.db';
const RECENT_LIMIT = 12;
const TOP_ARTISTS_LIMIT = 5;
const HISTORY_LIMIT = 1000;
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;


const fontTitle = gdi.Font('Segoe UI', 16, 1);
const fontText = gdi.Font('Segoe UI', 12, 0);
const fontSmall = gdi.Font('Segoe UI', 10, 0);

const tfArtist = fb.TitleFormat('$if2(%album artist%,$if2(%artist%,Unknown artist))');
const tfTitle = fb.TitleFormat('$if2(%title%,Unknown title)');
const tfAlbum = fb.TitleFormat('[%album%]');

let db = utils.OpenDatabase(DB_PATH);
let insertHistory = null;
let recent = [];
let topArtists = [];
let databaseError = '';

if (db) {
    try {
        db.Exec(`
            CREATE TABLE IF NOT EXISTS playback_history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                played_at INTEGER NOT NULL,
                artist TEXT NOT NULL,
                title TEXT NOT NULL,
                album TEXT NOT NULL,
                path TEXT NOT NULL
            );
            CREATE INDEX IF NOT EXISTS playback_history_played_at
                ON playback_history(played_at DESC);
        `);

        insertHistory = db.Prepare(`
            INSERT INTO playback_history(played_at, artist, title, album, path)
            VALUES (?, ?, ?, ?, ?)
        `);

        refreshView();
    } catch (e) {
        reportDatabaseError(e);

        if (insertHistory) {
            insertHistory.Close();
            insertHistory = null;
        }

        db.Close();
        db = null;
    }
}

function reportDatabaseError(error) {
    databaseError = error && error.message ? error.message : String(error);
    console.log(`SQLite Playback History: ${databaseError}`);
    window.Repaint();
}

function addPlayback(metadb) {
    if (!db || !insertHistory || !metadb) return;

    try {
        insertHistory.Run([
            Date.now(),
            tfArtist.EvalWithMetadb(metadb),
            tfTitle.EvalWithMetadb(metadb),
            tfAlbum.EvalWithMetadb(metadb),
            metadb.Path
        ]);

        // Keep the demo database bounded without rebuilding the table.
        db.Exec(`
            DELETE FROM playback_history
            WHERE id <= (
                SELECT id
                FROM playback_history
                ORDER BY id DESC
                LIMIT 1 OFFSET ?
            )
        `, [HISTORY_LIMIT - 1]);

        refreshView();
    } catch (e) {
        reportDatabaseError(e);
    }
}

function refreshView() {
    if (!db) return;

    const newRecent = db.Query(`
        SELECT played_at, artist, title, album
        FROM playback_history
        ORDER BY id DESC
        LIMIT ?
    `, [RECENT_LIMIT]);

    const newTopArtists = db.Query(`
        SELECT artist, COUNT(*) AS plays
        FROM playback_history
        WHERE played_at >= ?
        GROUP BY artist
        ORDER BY plays DESC, artist COLLATE NOCASE
        LIMIT ?
    `, [Date.now() - THIRTY_DAYS_MS, TOP_ARTISTS_LIMIT]);

    recent = newRecent;
    topArtists = newTopArtists;
    databaseError = '';
    window.Repaint();
}

function formatTime(timestamp) {
    return new Date(timestamp).toLocaleString();
}

function drawLine(gr, text, x, y, w, h, font, colour) {
    gr.GdiDrawText(
        text,
        font,
        colour,
        x,
        y,
        w,
        h,
        DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX
    );
}

function on_playback_new_track(metadb) {
    addPlayback(metadb);
}

function on_paint(gr) {
    const width = window.Width;
    const height = window.Height;
    const margin = 14;
    const text = RGB(230, 230, 230);
    const muted = RGB(155, 155, 155);
    const accent = RGB(110, 190, 255);
    const error = RGB(255, 130, 130);

    gr.FillSolidRect(0, 0, width, height, RGB(28, 30, 34));

    let y = 10;
    drawLine(gr, 'SQLite Playback History', margin, y, width - margin * 2, 28, fontTitle, text);
    y += 32;

    if (!db) {
        const message = databaseError
            ? `SQLite error: ${databaseError}`
            : 'Unable to open the playback history database.';
        drawLine(gr, message, margin, y, width - margin * 2, 22, fontText, error);
        return;
    }

    if (databaseError) {
        drawLine(gr, `SQLite error: ${databaseError}`, margin, y, width - margin * 2, 22, fontSmall, error);
        y += 24;
    }

    drawLine(gr, 'Top artists — last 30 days', margin, y, width - margin * 2, 20, fontSmall, muted);
    y += 20;

    if (!topArtists.length) {
        drawLine(gr, 'Play a few tracks to populate the database.', margin, y, width - margin * 2, 22, fontText, muted);
        y += 28;
    } else {
        for (const row of topArtists) {
            drawLine(gr, `${row.artist}  —  ${row.plays}`, margin, y, width - margin * 2, 22, fontText, accent);
            y += 22;
        }
        y += 8;
    }

    drawLine(gr, 'Recent plays', margin, y, width - margin * 2, 20, fontSmall, muted);
    y += 22;

    for (const row of recent) {
        if (y + 38 > height) break;

        drawLine(gr, `${row.artist} — ${row.title}`, margin, y, width - margin * 2, 20, fontText, text);
        y += 19;

        const album = row.album ? ` • ${row.album}` : '';
        drawLine(gr, `${formatTime(row.played_at)}${album}`, margin, y, width - margin * 2, 17, fontSmall, muted);
        y += 23;
    }
}

function on_mouse_rbtn_up(x, y) {
    if (!db) return;

    const menu = window.CreatePopupMenu();
    menu.AppendMenuItem(MF_STRING, 1, 'Clear history');

    if (menu.TrackPopupMenu(x, y) === 1) {
        try {
            db.Exec('DELETE FROM playback_history');
            refreshView();
        } catch (e) {
            reportDatabaseError(e);
        }
    }
}

function on_script_unload() {
    if (insertHistory) insertHistory.Close();
    if (db) db.Close();
}