Add DEBUG instrumentation and documentation

This commit is contained in:
Jordan Wages 2025-09-24 01:50:48 -05:00
commit e60222daec
3 changed files with 510 additions and 84 deletions

View file

@ -110,6 +110,14 @@ CREATE INDEX idx_artists_name_nocase ON artists(name COLLATE NOCASE);
- `createPagination` Standard pagination controls with page size selector.
- `Keyboard` helper Normalizes Enter/Esc handling for list rows and overlays.
- `createAsyncListState` Manages loading, error, and empty states for async lists.
- `prepareForView` Wraps `db.prepare` to attach view/label metadata for logging.
- `normalizePaginationState` / `clampPaginationToTotal` Normalizes and clamps pagination inputs while emitting DEBUG telemetry.
## Debugging & Logging
- Runtime diagnostics are gated behind a localStorage flag. Enable with `localStorage.setItem('mp3com.debug', 'true')` (or `'false'`/`removeItem` to disable) before reloading the app.
- When DEBUG is on, every SQLite statement logs `Query begin`/`Query end` entries (with view, SQL, params, row count, and duration) and captures detailed error payloads before rethrowing.
- Pagination helpers emit `[Pagination]` logs whenever raw inputs are normalized or clamped (including `clampedToTotal` flags when high page numbers are corrected after a count query).
- Use `prepareForView(db, VIEW_NAMES.someView, sql, label)` so statement metadata matches the originating view; this keeps console traces actionable.
## View Status
- [x] Navigation hub (`tpl-nav`, `createNavView`).

View file

@ -124,5 +124,12 @@ The `fts_tracks_*` tables are managed by SQLite to back the FTS5 virtual table.
- Restrict new CSS to scoped utility classes in `site.css`; otherwise rely on Bulma components.
- Before submitting changes, verify there are no console warnings, layout adapts from mobile to desktop, and keyboard navigation continues to work.
### Debug Instrumentation
- Enable verbose query diagnostics by setting `localStorage.setItem('mp3com.debug', 'true')` and reloading the app (clear the key or set it to `'false'` to disable).
- With DEBUG on, every SQLite statement logs `Query begin` / `Query end` entries that include the originating view, optional label, normalized SQL text, bound parameters, row counts, and execution duration.
- Any statement failure logs `[SQLite error]` details (SQL, params, phase, message, stack) before the exception propagates, making it easier to correlate with UI regressions.
- Pagination helpers emit `[Pagination]` traces whenever user inputs are normalized or clamped (including high-page corrections after a total count comes back smaller than expected).
- Use the `prepareForView(db, VIEW_NAMES.someView, sql, label)` helper when preparing new statements so that console output remains aligned with the feature area generating the query.
## License
Add licensing information here when available.

561
script.js
View file

@ -19,6 +19,216 @@
const IDB_STORE = 'files';
const IDB_KEY = 'mp3com-db-bytes-v1'; // bump if format changes
const DEBUG_STORAGE_KEY = 'mp3com.debug';
const DEBUG = (() => {
try {
const value = localStorage.getItem(DEBUG_STORAGE_KEY);
if (!value) return false;
const normalized = value.toLowerCase();
return normalized === '1' || normalized === 'true' || normalized === 'on';
} catch (_) {
return false;
}
})();
const now = typeof performance !== 'undefined' && performance && typeof performance.now === 'function'
? () => performance.now()
: () => Date.now();
function formatSql(sql) {
if (!sql) return '';
return String(sql).replace(/\s+/g, ' ').trim();
}
function normalizeParamValue(value) {
if (value === null || value === undefined) return value;
if (typeof value === 'string') {
const limit = 200;
if (value.length <= limit) return value;
return `${value.slice(0, limit)}`;
}
if (value instanceof Date) return value.toISOString();
if (ArrayBuffer.isView(value)) {
const ctor = value.constructor && value.constructor.name ? value.constructor.name : 'TypedArray';
return `<${ctor} length=${value.length}>`;
}
if (value instanceof ArrayBuffer) return `<ArrayBuffer byteLength=${value.byteLength}>`;
if (typeof value === 'object') {
const entries = Object.entries(value);
const limit = 20;
const result = {};
entries.slice(0, limit).forEach(([key, val]) => {
result[key] = normalizeParamValue(val);
});
if (entries.length > limit) result.__truncated = entries.length - limit;
return result;
}
return value;
}
function cloneBoundParams(values) {
if (values === undefined) return undefined;
if (Array.isArray(values)) return values.slice();
if (values && typeof values === 'object') return { ...values };
return values;
}
function formatParamsForLog(params) {
if (params === undefined) return undefined;
if (Array.isArray(params)) return params.map((value) => normalizeParamValue(value));
if (params && typeof params === 'object') {
const result = {};
Object.keys(params).forEach((key) => {
result[key] = normalizeParamValue(params[key]);
});
return result;
}
return [normalizeParamValue(params)];
}
function logDebug(event, payload) {
if (!DEBUG) return;
console.debug(event, payload);
}
function logSqlError({ view, sql, params, error, phase }) {
if (!DEBUG) return;
console.error('[SQLite error]', {
view: view || 'unknown',
phase: phase || 'unknown',
sql: formatSql(sql),
params: formatParamsForLog(params),
message: error && error.message ? error.message : String(error),
stack: error && error.stack ? error.stack : undefined,
});
}
function logQueryBegin({ view, sql, params, label }) {
logDebug('[Query begin]', {
view: view || 'unknown',
label: label || undefined,
sql: formatSql(sql),
params: formatParamsForLog(params),
});
}
function logQueryEnd({ view, sql, params, rowCount, durationMs, label, errored }) {
logDebug('[Query end]', {
view: view || 'unknown',
label: label || undefined,
sql: formatSql(sql),
params: formatParamsForLog(params),
rows: rowCount,
durationMs: Math.round(durationMs * 1000) / 1000,
errored: !!errored,
});
}
function logPaginationState({ view, rawPage, rawPageSize, page, pageSize, offset, flags }) {
logDebug('[Pagination]', {
view: view || 'unknown',
rawPage,
rawPageSize,
page,
pageSize,
offset,
flags: flags || undefined,
});
}
function normalizePagination({ page, pageSize, defaultPageSize, view }) {
const safeDefault = Number.isFinite(defaultPageSize) && defaultPageSize > 0 ? Math.max(1, Math.floor(defaultPageSize)) : 25;
const rawPage = page;
const rawPageSize = pageSize;
const numericPage = Number(page);
const numericPageSize = Number(pageSize);
const flags = {
invalidPage: !Number.isFinite(numericPage) || numericPage <= 0,
invalidPageSize: !Number.isFinite(numericPageSize) || numericPageSize <= 0,
nonIntegerPage: !Number.isInteger(numericPage),
nonIntegerPageSize: !Number.isInteger(numericPageSize),
};
let normalizedPageSize = flags.invalidPageSize ? safeDefault : Math.max(1, Math.floor(numericPageSize));
if (normalizedPageSize !== numericPageSize) flags.clampedPageSize = true;
let normalizedPage = flags.invalidPage ? 1 : Math.max(1, Math.floor(numericPage));
if (normalizedPage !== numericPage) flags.clampedPage = true;
const offset = Math.max(0, (normalizedPage - 1) * normalizedPageSize);
logPaginationState({
view,
rawPage,
rawPageSize,
page: normalizedPage,
pageSize: normalizedPageSize,
offset,
flags,
});
return {
page: normalizedPage,
pageSize: normalizedPageSize,
offset,
flags,
};
}
function normalizePaginationState({ state, defaultPageSize, view }) {
const result = normalizePagination({
page: state.page,
pageSize: state.pageSize,
defaultPageSize,
view,
});
if (state) {
state.page = result.page;
state.pageSize = result.pageSize;
}
return result;
}
function clampPaginationToTotal({ state, total, pageSize, view }) {
if (!state || !Number.isFinite(total) || total < 0) return false;
const numericPage = Number(state.page);
if (!Number.isFinite(numericPage) || numericPage < 1) return false;
const safePageSize = Number.isFinite(pageSize) && pageSize > 0 ? pageSize : 1;
const maxPage = Math.max(1, Math.ceil(total / safePageSize));
if (numericPage <= maxPage) return false;
const clampedPage = maxPage;
const offset = Math.max(0, (clampedPage - 1) * safePageSize);
logPaginationState({
view,
rawPage: state.page,
rawPageSize: state.pageSize,
page: clampedPage,
pageSize: safePageSize,
offset,
flags: { clampedToTotal: true },
});
state.page = clampedPage;
return true;
}
const VIEW_NAMES = {
nav: 'nav',
search: 'search',
browseArtists: 'browseArtists',
browseAlbums: 'browseAlbums',
browseYears: 'browseYears',
browseGenres: 'browseGenres',
stats: 'stats',
artistOverlay: 'artistOverlay',
albumOverlay: 'albumOverlay',
trackOverlay: 'trackOverlay',
};
function prepareForView(db, view, sql, label) {
const context = label ? { view, label } : { view };
return db.prepare(sql, context);
}
// Viewport layers
const $uxRoot = document.getElementById('ux-root');
const $uxOverlays = document.getElementById('ux-overlays');
@ -320,18 +530,49 @@
function createSqlCompat(SQL) {
class Statement {
constructor(stmt) {
constructor(stmt, meta = {}) {
this._stmt = stmt;
this._columnNames = null;
this._meta = {
sql: meta.sql || '',
view: meta.view || 'unknown',
label: meta.label || undefined,
};
this._boundParams = undefined;
this._rowCount = 0;
this._startedAt = 0;
this._errored = false;
}
setContext(meta = {}) {
if (!meta) return this;
if (meta.sql) this._meta.sql = meta.sql;
if (meta.view) this._meta.view = meta.view;
if (Object.prototype.hasOwnProperty.call(meta, 'label')) this._meta.label = meta.label;
return this;
}
bind(values) {
try {
if (values !== undefined) this._stmt.bind(values);
this._boundParams = cloneBoundParams(values);
return true;
} catch (error) {
this._recordError(error, 'bind');
throw error;
}
}
step() {
return !!this._stmt.step();
try {
this._ensureBegun();
const hasRow = this._stmt.step();
if (hasRow) this._rowCount += 1;
return !!hasRow;
} catch (error) {
this._recordError(error, 'step');
throw error;
}
}
getAsObject() {
try {
if (!this._columnNames) this._columnNames = this._stmt.getColumnNames();
const row = Object.create(null);
const count = this._columnNames.length;
@ -339,9 +580,57 @@
row[this._columnNames[i]] = this._stmt.get(i);
}
return row;
} catch (error) {
this._recordError(error, 'getAsObject');
throw error;
}
}
free() {
try {
this._stmt.free();
} catch (error) {
this._recordError(error, 'free');
throw error;
} finally {
this._finalizeLifecycle();
this._columnNames = null;
}
}
_ensureBegun() {
if (this._startedAt) return;
this._startedAt = now();
logQueryBegin({
view: this._meta.view,
sql: this._meta.sql,
params: this._boundParams,
label: this._meta.label,
});
}
_finalizeLifecycle() {
if (!this._startedAt) return;
const duration = Math.max(0, now() - this._startedAt);
logQueryEnd({
view: this._meta.view,
sql: this._meta.sql,
params: this._boundParams,
rowCount: this._rowCount,
durationMs: duration,
label: this._meta.label,
errored: this._errored,
});
this._startedAt = 0;
this._rowCount = 0;
this._errored = false;
}
_recordError(error, phase) {
this._errored = true;
logSqlError({
view: this._meta.view,
sql: this._meta.sql,
params: this._boundParams,
error,
phase,
});
}
}
@ -350,8 +639,9 @@
const source = bytes instanceof Uint8Array ? bytes : bytes ? new Uint8Array(bytes) : undefined;
this._db = source ? new SQL.Database(source) : new SQL.Database();
}
prepare(sql) {
return new Statement(this._db.prepare(sql));
prepare(sql, context) {
const meta = context ? { ...context, sql } : { sql };
return new Statement(this._db.prepare(sql), meta);
}
exec(sql) {
return this._db.exec(sql);
@ -364,7 +654,10 @@
function ensureFts5Available(dbHandle) {
let stmt;
try {
stmt = dbHandle.prepare('CREATE VIRTUAL TABLE temp.__fts5_check USING fts5(content)');
stmt = dbHandle.prepare('CREATE VIRTUAL TABLE temp.__fts5_check USING fts5(content)', {
view: 'bootstrap',
label: 'fts5-check-create',
});
stmt.step();
} catch (err) {
const reason = err && err.message ? err.message : String(err);
@ -847,10 +1140,11 @@
const listState = createAsyncListState({ table, statusEl: $status, pagination });
const defaultPageSize = Math.max(1, Number(pagination.pageSize) || 25);
const state = {
query: initialQuery,
page: 1,
pageSize: pagination.pageSize,
pageSize: defaultPageSize,
sort: 'rank',
};
@ -953,17 +1247,25 @@
listState.showLoading('Searching…');
const offset = (state.page - 1) * state.pageSize;
let { page, pageSize, offset } = normalizePaginationState({
state,
defaultPageSize,
view: VIEW_NAMES.search,
});
const effectiveSearchSql = searchSql[state.sort] || searchSql.rank;
let total = 0;
let rows = [];
try {
console.debug('[Search] Preparing count query', {
logDebug('[Search] count SQL', {
matchExpr,
sql: countSql.trim(),
ftsExpr: matchExpr,
chars: Array.from(matchExpr).map((ch) => ch.codePointAt(0)),
});
const countStmt = db.prepare(applyFtsMatch(countSql, matchExpr));
const countStmt = prepareForView(
db,
VIEW_NAMES.search,
applyFtsMatch(countSql, matchExpr),
'count',
);
if (countStmt.step()) {
const row = countStmt.getAsObject();
total = Number(row.count) || 0;
@ -976,18 +1278,31 @@
return;
}
if (offset >= total) {
state.page = Math.max(1, Math.ceil(total / state.pageSize));
if (clampPaginationToTotal({
state,
total,
pageSize,
view: VIEW_NAMES.search,
})) {
({ page, pageSize, offset } = normalizePaginationState({
state,
defaultPageSize,
view: VIEW_NAMES.search,
}));
}
const searchParams = [state.pageSize, (state.page - 1) * state.pageSize];
console.debug('[Search] Preparing row query', {
const searchParams = [pageSize, offset];
logDebug('[Search] row SQL', {
matchExpr,
sql: effectiveSearchSql.trim(),
ftsExpr: matchExpr,
params: searchParams,
chars: Array.from(matchExpr).map((ch) => ch.codePointAt(0)),
});
const searchStmt = db.prepare(applyFtsMatch(effectiveSearchSql, matchExpr));
const searchStmt = prepareForView(
db,
VIEW_NAMES.search,
applyFtsMatch(effectiveSearchSql, matchExpr),
'rows',
);
searchStmt.bind(searchParams);
const nextRows = [];
while (searchStmt.step()) {
@ -1004,8 +1319,8 @@
listState.showRows({
rows,
total,
page: state.page,
pageSize: state.pageSize,
page,
pageSize,
});
}
@ -1123,16 +1438,11 @@
};
function getNormalizedPagination() {
let pageSize = Number(state.pageSize);
if (!Number.isFinite(pageSize) || pageSize <= 0) pageSize = defaultPageSize;
else pageSize = Math.max(1, Math.floor(pageSize));
let page = Number(state.page);
if (!Number.isFinite(page) || page <= 0) page = 1;
else page = Math.max(1, Math.floor(page));
if (state.pageSize !== pageSize) state.pageSize = pageSize;
if (state.page !== page) state.page = page;
const offset = Math.max(0, (page - 1) * pageSize);
return { page, pageSize, offset };
return normalizePaginationState({
state,
defaultPageSize,
view: VIEW_NAMES.browseArtists,
});
}
function escapeLike(str) {
@ -1216,16 +1526,30 @@
try {
if (ftsMatch) {
const ftsCountStmt = db.prepare(applyFtsMatch(ftsCountSql, ftsMatch));
const ftsCountStmt = prepareForView(
db,
VIEW_NAMES.browseArtists,
applyFtsMatch(ftsCountSql, ftsMatch),
'fts-count',
);
if (ftsCountStmt.step()) total = Number(ftsCountStmt.getAsObject().count) || 0;
ftsCountStmt.free();
if (total > 0) {
if (offset >= total) {
state.page = Math.max(1, Math.ceil(total / pageSize));
if (clampPaginationToTotal({
state,
total,
pageSize,
view: VIEW_NAMES.browseArtists,
})) {
({ page, pageSize, offset } = getNormalizedPagination());
}
const ftsRowsStmt = db.prepare(applyFtsMatch(ftsRowsSql, ftsMatch));
const ftsRowsStmt = prepareForView(
db,
VIEW_NAMES.browseArtists,
applyFtsMatch(ftsRowsSql, ftsMatch),
'fts-rows',
);
ftsRowsStmt.bind([pageSize, offset]);
while (ftsRowsStmt.step()) rows.push(ftsRowsStmt.getAsObject());
ftsRowsStmt.free();
@ -1234,7 +1558,12 @@
}
if (!usedFts) {
const countStmt = db.prepare(useUnfilteredQuery ? baseCountSql : countSql);
const countStmt = prepareForView(
db,
VIEW_NAMES.browseArtists,
useUnfilteredQuery ? baseCountSql : countSql,
useUnfilteredQuery ? 'count' : 'count-filtered',
);
if (!useUnfilteredQuery) countStmt.bind([likeTerm]);
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
countStmt.free();
@ -1245,12 +1574,21 @@
return;
}
if (offset >= total) {
state.page = Math.max(1, Math.ceil(total / pageSize));
if (clampPaginationToTotal({
state,
total,
pageSize,
view: VIEW_NAMES.browseArtists,
})) {
({ page, pageSize, offset } = getNormalizedPagination());
}
const rowsStmt = db.prepare(useUnfilteredQuery ? baseRowsSql : rowsSql);
const rowsStmt = prepareForView(
db,
VIEW_NAMES.browseArtists,
useUnfilteredQuery ? baseRowsSql : rowsSql,
useUnfilteredQuery ? 'rows' : 'rows-filtered',
);
if (useUnfilteredQuery) rowsStmt.bind([pageSize, offset]);
else rowsStmt.bind([likeTerm, pageSize, offset]);
while (rowsStmt.step()) rows.push(rowsStmt.getAsObject());
@ -1359,10 +1697,11 @@
const listState = createAsyncListState({ table, statusEl: $status, pagination });
const defaultPageSize = Math.max(1, Number(pagination.pageSize) || 25);
const state = {
sort: initialSort,
page: 1,
pageSize: pagination.pageSize,
pageSize: defaultPageSize,
};
const orderMap = {
@ -1383,10 +1722,15 @@
function loadAlbumsImmediate() {
listState.showLoading('Loading…');
let { page, pageSize, offset } = normalizePaginationState({
state,
defaultPageSize,
view: VIEW_NAMES.browseAlbums,
});
let total = 0;
const rows = [];
try {
const countStmt = db.prepare(countSql);
const countStmt = prepareForView(db, VIEW_NAMES.browseAlbums, countSql, 'count');
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
countStmt.free();
@ -1396,12 +1740,27 @@
return;
}
const maxPage = Math.max(1, Math.ceil(total / state.pageSize));
if (state.page > maxPage) state.page = maxPage;
if (clampPaginationToTotal({
state,
total,
pageSize,
view: VIEW_NAMES.browseAlbums,
})) {
({ page, pageSize, offset } = normalizePaginationState({
state,
defaultPageSize,
view: VIEW_NAMES.browseAlbums,
}));
}
const order = orderMap[state.sort] || orderMap.artist;
const stmt = db.prepare(baseSql.replace('%ORDER%', order));
stmt.bind([state.pageSize, (state.page - 1) * state.pageSize]);
const stmt = prepareForView(
db,
VIEW_NAMES.browseAlbums,
baseSql.replace('%ORDER%', order),
'rows',
);
stmt.bind([pageSize, offset]);
while (stmt.step()) rows.push(stmt.getAsObject());
stmt.free();
} catch (err) {
@ -1413,8 +1772,8 @@
listState.showRows({
rows,
total,
page: state.page,
pageSize: state.pageSize,
page,
pageSize,
});
}
@ -1495,11 +1854,12 @@
const trackListState = createAsyncListState({ table: trackTable, statusEl: $tracksStatus, pagination });
const defaultPageSize = Math.max(1, Number(pagination.pageSize) || 25);
const state = {
years: [],
selectedYear: presetYear,
page: 1,
pageSize: pagination.pageSize,
pageSize: defaultPageSize,
};
let yearButtons = [];
@ -1571,7 +1931,7 @@
function loadYears() {
try {
const stmt = db.prepare(yearsSql);
const stmt = prepareForView(db, VIEW_NAMES.browseYears, yearsSql, 'year-list');
const data = [];
while (stmt.step()) data.push(stmt.getAsObject());
stmt.free();
@ -1592,10 +1952,20 @@
trackListState.showLoading('Loading tracks…');
let { page, pageSize, offset } = normalizePaginationState({
state,
defaultPageSize,
view: VIEW_NAMES.browseYears,
});
let total = 0;
const rows = [];
try {
const countStmt = db.prepare(tracksCountSql);
const countStmt = prepareForView(
db,
VIEW_NAMES.browseYears,
tracksCountSql,
'tracks-count',
);
countStmt.bind([state.selectedYear]);
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
countStmt.free();
@ -1606,11 +1976,26 @@
return;
}
const maxPage = Math.max(1, Math.ceil(total / state.pageSize));
if (state.page > maxPage) state.page = maxPage;
if (clampPaginationToTotal({
state,
total,
pageSize,
view: VIEW_NAMES.browseYears,
})) {
({ page, pageSize, offset } = normalizePaginationState({
state,
defaultPageSize,
view: VIEW_NAMES.browseYears,
}));
}
const stmt = db.prepare(tracksRowsSql);
stmt.bind([state.selectedYear, state.pageSize, (state.page - 1) * state.pageSize]);
const stmt = prepareForView(
db,
VIEW_NAMES.browseYears,
tracksRowsSql,
'tracks-rows',
);
stmt.bind([state.selectedYear, pageSize, offset]);
while (stmt.step()) rows.push(stmt.getAsObject());
stmt.free();
} catch (err) {
@ -1622,8 +2007,8 @@
trackListState.showRows({
rows,
total,
page: state.page,
pageSize: state.pageSize,
page,
pageSize,
});
}
@ -1700,11 +2085,12 @@
const trackListState = createAsyncListState({ table: trackTable, statusEl: $tracksStatus, pagination });
const defaultPageSize = Math.max(1, Number(pagination.pageSize) || 25);
const state = {
genres: [],
selectedGenre: presetGenre,
page: 1,
pageSize: pagination.pageSize,
pageSize: defaultPageSize,
};
let genreButtons = [];
@ -1768,7 +2154,7 @@
function loadGenres() {
try {
const stmt = db.prepare(genresSql);
const stmt = prepareForView(db, VIEW_NAMES.browseGenres, genresSql, 'genre-list');
const rows = [];
while (stmt.step()) rows.push(stmt.getAsObject());
stmt.free();
@ -1792,10 +2178,20 @@
$selectedGenre.textContent = `Tracks tagged ${state.selectedGenre}`;
trackListState.showLoading('Loading tracks…');
let { page, pageSize, offset } = normalizePaginationState({
state,
defaultPageSize,
view: VIEW_NAMES.browseGenres,
});
let total = 0;
const rows = [];
try {
const countStmt = db.prepare(tracksCountSql);
const countStmt = prepareForView(
db,
VIEW_NAMES.browseGenres,
tracksCountSql,
'tracks-count',
);
countStmt.bind([state.selectedGenre]);
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
countStmt.free();
@ -1806,11 +2202,26 @@
return;
}
const maxPage = Math.max(1, Math.ceil(total / state.pageSize));
if (state.page > maxPage) state.page = maxPage;
if (clampPaginationToTotal({
state,
total,
pageSize,
view: VIEW_NAMES.browseGenres,
})) {
({ page, pageSize, offset } = normalizePaginationState({
state,
defaultPageSize,
view: VIEW_NAMES.browseGenres,
}));
}
const stmt = db.prepare(tracksRowsSql);
stmt.bind([state.selectedGenre, state.pageSize, (state.page - 1) * state.pageSize]);
const stmt = prepareForView(
db,
VIEW_NAMES.browseGenres,
tracksRowsSql,
'tracks-rows',
);
stmt.bind([state.selectedGenre, pageSize, offset]);
while (stmt.step()) rows.push(stmt.getAsObject());
stmt.free();
} catch (err) {
@ -1822,8 +2233,8 @@
trackListState.showRows({
rows,
total,
page: state.page,
pageSize: state.pageSize,
page,
pageSize,
});
}
@ -1893,7 +2304,7 @@
function loadPrecomputedStats() {
try {
const stmt = db.prepare(siteStatsSql);
const stmt = prepareForView(db, VIEW_NAMES.stats, siteStatsSql, 'site-stats');
const map = new Map();
try {
while (stmt.step()) {
@ -1950,7 +2361,7 @@
}
try {
const stmt = db.prepare(totalsSql);
const stmt = prepareForView(db, VIEW_NAMES.stats, totalsSql, 'totals');
stmt.step();
const totals = stmt.getAsObject();
stmt.free();
@ -2031,17 +2442,17 @@
if (!precomputedArtists || !precomputedYears || !precomputedGenres) {
try {
if (!precomputedArtists) {
const artistStmt = db.prepare(topArtistsSql);
const artistStmt = prepareForView(db, VIEW_NAMES.stats, topArtistsSql, 'top-artists');
while (artistStmt.step()) topArtists.push(artistStmt.getAsObject());
artistStmt.free();
}
if (!precomputedYears) {
const yearStmt = db.prepare(topYearsSql);
const yearStmt = prepareForView(db, VIEW_NAMES.stats, topYearsSql, 'top-years');
while (yearStmt.step()) topYears.push(yearStmt.getAsObject());
yearStmt.free();
}
if (!precomputedGenres) {
const genreStmt = db.prepare(topGenresSql);
const genreStmt = prepareForView(db, VIEW_NAMES.stats, topGenresSql, 'top-genres');
while (genreStmt.step()) topGenres.push(genreStmt.getAsObject());
genreStmt.free();
}
@ -2200,7 +2611,7 @@
function loadHeader() {
try {
const stmt = db.prepare(headerSql);
const stmt = prepareForView(db, VIEW_NAMES.artistOverlay, headerSql, 'artist-header');
stmt.bind([artistId]);
if (stmt.step()) {
artistInfo = stmt.getAsObject();
@ -2224,7 +2635,7 @@
loaded.add('albums');
try {
const rows = [];
const stmt = db.prepare(albumsSql);
const stmt = prepareForView(db, VIEW_NAMES.artistOverlay, albumsSql, 'artist-albums');
stmt.bind([artistId]);
while (stmt.step()) rows.push(stmt.getAsObject());
stmt.free();
@ -2242,7 +2653,7 @@
loaded.add('tracks');
try {
const rows = [];
const stmt = db.prepare(tracksSql);
const stmt = prepareForView(db, VIEW_NAMES.artistOverlay, tracksSql, 'artist-tracks');
stmt.bind([artistId]);
while (stmt.step()) rows.push(stmt.getAsObject());
stmt.free();
@ -2353,7 +2764,7 @@
function loadHeader() {
try {
const stmt = db.prepare(headerSql);
const stmt = prepareForView(db, VIEW_NAMES.albumOverlay, headerSql, 'album-header');
stmt.bind([albumId]);
if (stmt.step()) {
const info = stmt.getAsObject();
@ -2377,7 +2788,7 @@
function loadTracks() {
try {
const rows = [];
const stmt = db.prepare(tracksSql);
const stmt = prepareForView(db, VIEW_NAMES.albumOverlay, tracksSql, 'album-tracks');
stmt.bind([albumId]);
while (stmt.step()) rows.push(stmt.getAsObject());
stmt.free();
@ -2464,7 +2875,7 @@
function loadTrack() {
try {
const stmt = db.prepare(sql);
const stmt = prepareForView(db, VIEW_NAMES.trackOverlay, sql, 'track-detail');
stmt.bind([trackId]);
if (stmt.step()) {
track = stmt.getAsObject();