Add DEBUG instrumentation and documentation
This commit is contained in:
parent
c5d4021e18
commit
e60222daec
3 changed files with 510 additions and 84 deletions
579
script.js
579
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 `<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,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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue