diff --git a/AGENTS.md b/AGENTS.md index 517dc8c..d28387e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`). diff --git a/README.md b/README.md index 329d9c7..3d986cf 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/script.js b/script.js index 9882e3e..55b5111 100644 --- a/script.js +++ b/script.js @@ -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 ``; + 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,28 +530,107 @@ 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) { - if (values !== undefined) this._stmt.bind(values); - return true; + 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() { - if (!this._columnNames) this._columnNames = this._stmt.getColumnNames(); - const row = Object.create(null); - const count = this._columnNames.length; - for (let i = 0; i < count; i += 1) { - row[this._columnNames[i]] = this._stmt.get(i); + try { + if (!this._columnNames) this._columnNames = this._stmt.getColumnNames(); + const row = Object.create(null); + const count = this._columnNames.length; + for (let i = 0; i < count; i += 1) { + row[this._columnNames[i]] = this._stmt.get(i); + } + return row; + } catch (error) { + this._recordError(error, 'getAsObject'); + throw error; } - return row; } free() { - this._stmt.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();