Add SQL helper utilities and migrate queries
This commit is contained in:
parent
d8ecf5f607
commit
fa68916d3e
2 changed files with 218 additions and 136 deletions
353
script.js
353
script.js
|
@ -124,6 +124,89 @@
|
|||
});
|
||||
}
|
||||
|
||||
const SQL_FRAGMENT = Symbol('sqlFragment');
|
||||
|
||||
function createSqlFragment(text, raw) {
|
||||
return {
|
||||
text,
|
||||
raw,
|
||||
[SQL_FRAGMENT]: true,
|
||||
};
|
||||
}
|
||||
|
||||
function isSqlFragment(value) {
|
||||
return Boolean(value && value[SQL_FRAGMENT]);
|
||||
}
|
||||
|
||||
function quoteSqlString(value) {
|
||||
return `'${String(value).replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function sqlValue(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return createSqlFragment('NULL', value);
|
||||
}
|
||||
const type = typeof value;
|
||||
if (type === 'number') {
|
||||
if (!Number.isFinite(value)) throw new Error(`Invalid numeric SQL value: ${value}`);
|
||||
return createSqlFragment(String(value), value);
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return createSqlFragment(value ? '1' : '0', value);
|
||||
}
|
||||
if (type === 'string') {
|
||||
return createSqlFragment(quoteSqlString(value), value);
|
||||
}
|
||||
throw new Error(`Unsupported SQL value type: ${type}`);
|
||||
}
|
||||
|
||||
function sqlLike(value, { escapeChar = '\\' } = {}) {
|
||||
if (escapeChar === null || escapeChar === undefined) throw new Error('escapeChar required');
|
||||
if (typeof escapeChar !== 'string' || escapeChar.length !== 1) throw new Error('escapeChar must be a single character');
|
||||
if (value === null || value === undefined) {
|
||||
return createSqlFragment('NULL', { value, escapeChar });
|
||||
}
|
||||
const raw = String(value);
|
||||
const regex = new RegExp(`[${escapeChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}%_]`, 'g');
|
||||
const escaped = raw.replace(regex, (match) => escapeChar + match);
|
||||
return createSqlFragment(quoteSqlString(escaped), { value, escapeChar });
|
||||
}
|
||||
|
||||
function sqlIdentifier(name, { allowlist } = {}) {
|
||||
if (typeof name !== 'string' || !name) throw new Error('Identifier must be a non-empty string');
|
||||
if (!Array.isArray(allowlist) || allowlist.length === 0) throw new Error('allowlist required for sqlIdentifier');
|
||||
if (!allowlist.includes(name)) {
|
||||
throw new Error(`Identifier not in allowlist: ${name}`);
|
||||
}
|
||||
return createSqlFragment(name, name);
|
||||
}
|
||||
|
||||
function buildSql(strings, ...values) {
|
||||
if (!Array.isArray(strings) || strings.length === 0) throw new Error('sql tag requires template literals');
|
||||
let sqlText = '';
|
||||
const params = [];
|
||||
for (let i = 0; i < strings.length; i += 1) {
|
||||
sqlText += strings[i];
|
||||
if (i >= values.length) continue;
|
||||
const part = values[i];
|
||||
if (isSqlFragment(part)) {
|
||||
sqlText += part.text;
|
||||
params.push(part.raw);
|
||||
continue;
|
||||
}
|
||||
if (part && typeof part === 'object' && typeof part.sql === 'string') {
|
||||
sqlText += part.sql;
|
||||
if (Array.isArray(part.params)) params.push(...part.params);
|
||||
else if (part.params !== undefined) params.push(part.params);
|
||||
continue;
|
||||
}
|
||||
throw new Error('sql template values must be created with sqlValue/sqlLike/sqlIdentifier or other sql fragments');
|
||||
}
|
||||
return { sql: sqlText, params };
|
||||
}
|
||||
|
||||
const sql = buildSql;
|
||||
|
||||
function logPaginationState({ view, rawPage, rawPageSize, page, pageSize, offset, flags }) {
|
||||
logDebug('[Pagination]', {
|
||||
view: view || 'unknown',
|
||||
|
@ -136,6 +219,20 @@
|
|||
});
|
||||
}
|
||||
|
||||
function normalizeSqlInput(sqlInput) {
|
||||
if (typeof sqlInput === 'string') {
|
||||
return { sql: sqlInput, params: undefined };
|
||||
}
|
||||
if (sqlInput && typeof sqlInput === 'object') {
|
||||
const sqlText = sqlInput.sql;
|
||||
if (typeof sqlText !== 'string' || !sqlText) {
|
||||
throw new Error('Invalid SQL input: missing sql string');
|
||||
}
|
||||
return { sql: sqlText, params: cloneBoundParams(sqlInput.params) };
|
||||
}
|
||||
throw new Error('Invalid SQL input');
|
||||
}
|
||||
|
||||
function normalizePagination({ page, pageSize, defaultPageSize, view }) {
|
||||
const safeDefault = Number.isFinite(defaultPageSize) && defaultPageSize > 0 ? Math.max(1, Math.floor(defaultPageSize)) : 25;
|
||||
const rawPage = page;
|
||||
|
@ -492,16 +589,6 @@
|
|||
return `${hz.toLocaleString()} Hz`;
|
||||
}
|
||||
|
||||
function escapeSqlText(value) {
|
||||
return `'${String(value).replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function applyFtsMatch(sql, matchExpr) {
|
||||
const placeholder = 'MATCH ?';
|
||||
if (!sql.includes(placeholder)) throw new Error('Expected FTS MATCH placeholder');
|
||||
return sql.replace(placeholder, `MATCH ${escapeSqlText(matchExpr)}`);
|
||||
}
|
||||
|
||||
async function unzipSqlite(zipBytes) {
|
||||
loader.setStep('Unpacking database…');
|
||||
loader.setDetail('Decompressing ZIP');
|
||||
|
@ -538,7 +625,7 @@
|
|||
view: meta.view || 'unknown',
|
||||
label: meta.label || undefined,
|
||||
};
|
||||
this._boundParams = undefined;
|
||||
this._paramsForLog = cloneBoundParams(meta.params);
|
||||
this._rowCount = 0;
|
||||
this._startedAt = 0;
|
||||
this._errored = false;
|
||||
|
@ -548,17 +635,10 @@
|
|||
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;
|
||||
if (Object.prototype.hasOwnProperty.call(meta, 'params')) {
|
||||
this._paramsForLog = cloneBoundParams(meta.params);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
step() {
|
||||
try {
|
||||
|
@ -602,7 +682,7 @@
|
|||
logQueryBegin({
|
||||
view: this._meta.view,
|
||||
sql: this._meta.sql,
|
||||
params: this._boundParams,
|
||||
params: this._paramsForLog,
|
||||
label: this._meta.label,
|
||||
});
|
||||
}
|
||||
|
@ -612,7 +692,7 @@
|
|||
logQueryEnd({
|
||||
view: this._meta.view,
|
||||
sql: this._meta.sql,
|
||||
params: this._boundParams,
|
||||
params: this._paramsForLog,
|
||||
rowCount: this._rowCount,
|
||||
durationMs: duration,
|
||||
label: this._meta.label,
|
||||
|
@ -627,7 +707,7 @@
|
|||
logSqlError({
|
||||
view: this._meta.view,
|
||||
sql: this._meta.sql,
|
||||
params: this._boundParams,
|
||||
params: this._paramsForLog,
|
||||
error,
|
||||
phase,
|
||||
});
|
||||
|
@ -639,9 +719,10 @@
|
|||
const source = bytes instanceof Uint8Array ? bytes : bytes ? new Uint8Array(bytes) : undefined;
|
||||
this._db = source ? new SQL.Database(source) : new SQL.Database();
|
||||
}
|
||||
prepare(sql, context) {
|
||||
const meta = context ? { ...context, sql } : { sql };
|
||||
return new Statement(this._db.prepare(sql), meta);
|
||||
prepare(sqlInput, context) {
|
||||
const { sql: sqlText, params } = normalizeSqlInput(sqlInput);
|
||||
const meta = context ? { ...context, sql: sqlText, params } : { sql: sqlText, params };
|
||||
return new Statement(this._db.prepare(sqlText), meta);
|
||||
}
|
||||
exec(sql) {
|
||||
return this._db.exec(sql);
|
||||
|
@ -1179,34 +1260,38 @@
|
|||
}
|
||||
|
||||
const searchSql = {
|
||||
rank: `
|
||||
SELECT t.id, a.name AS artist, t.title, IFNULL(al.title, '') AS album, t.year, t.genre
|
||||
FROM fts_tracks
|
||||
JOIN tracks t ON t.id = fts_tracks.rowid
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
LEFT JOIN albums al ON al.id = t.album_id
|
||||
WHERE fts_tracks MATCH ?
|
||||
ORDER BY rank
|
||||
LIMIT ? OFFSET ?
|
||||
`,
|
||||
alpha: `
|
||||
SELECT t.id, a.name AS artist, t.title, IFNULL(al.title, '') AS album, t.year, t.genre
|
||||
FROM fts_tracks
|
||||
JOIN tracks t ON t.id = fts_tracks.rowid
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
LEFT JOIN albums al ON al.id = t.album_id
|
||||
WHERE fts_tracks MATCH ?
|
||||
ORDER BY a.name COLLATE NOCASE, t.title COLLATE NOCASE, COALESCE(t.year, 0)
|
||||
LIMIT ? OFFSET ?
|
||||
`,
|
||||
rank({ matchExpr, pageSize, offset }) {
|
||||
return sql`
|
||||
SELECT t.id, a.name AS artist, t.title, IFNULL(al.title, '') AS album, t.year, t.genre
|
||||
FROM fts_tracks
|
||||
JOIN tracks t ON t.id = fts_tracks.rowid
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
LEFT JOIN albums al ON al.id = t.album_id
|
||||
WHERE fts_tracks MATCH ${sqlValue(matchExpr)}
|
||||
ORDER BY rank
|
||||
LIMIT ${sqlValue(pageSize)} OFFSET ${sqlValue(offset)}
|
||||
`;
|
||||
},
|
||||
alpha({ matchExpr, pageSize, offset }) {
|
||||
return sql`
|
||||
SELECT t.id, a.name AS artist, t.title, IFNULL(al.title, '') AS album, t.year, t.genre
|
||||
FROM fts_tracks
|
||||
JOIN tracks t ON t.id = fts_tracks.rowid
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
LEFT JOIN albums al ON al.id = t.album_id
|
||||
WHERE fts_tracks MATCH ${sqlValue(matchExpr)}
|
||||
ORDER BY a.name COLLATE NOCASE, t.title COLLATE NOCASE, COALESCE(t.year, 0)
|
||||
LIMIT ${sqlValue(pageSize)} OFFSET ${sqlValue(offset)}
|
||||
`;
|
||||
},
|
||||
};
|
||||
const countSql = `
|
||||
const countSql = ({ matchExpr }) => sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM fts_tracks
|
||||
JOIN tracks t ON t.id = fts_tracks.rowid
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
LEFT JOIN albums al ON al.id = t.album_id
|
||||
WHERE fts_tracks MATCH ?
|
||||
WHERE fts_tracks MATCH ${sqlValue(matchExpr)}
|
||||
`;
|
||||
|
||||
function enableSearch() {
|
||||
|
@ -1252,20 +1337,17 @@
|
|||
defaultPageSize,
|
||||
view: VIEW_NAMES.search,
|
||||
});
|
||||
const effectiveSearchSql = searchSql[state.sort] || searchSql.rank;
|
||||
const effectiveSearchBuilder = searchSql[state.sort] || searchSql.rank;
|
||||
let total = 0;
|
||||
let rows = [];
|
||||
try {
|
||||
const countQuery = countSql({ matchExpr });
|
||||
logDebug('[Search] count SQL', {
|
||||
matchExpr,
|
||||
sql: countSql.trim(),
|
||||
sql: formatSql(countQuery.sql),
|
||||
params: formatParamsForLog(countQuery.params),
|
||||
});
|
||||
const countStmt = prepareForView(
|
||||
db,
|
||||
VIEW_NAMES.search,
|
||||
applyFtsMatch(countSql, matchExpr),
|
||||
'count',
|
||||
);
|
||||
const countStmt = prepareForView(db, VIEW_NAMES.search, countQuery, 'count');
|
||||
if (countStmt.step()) {
|
||||
const row = countStmt.getAsObject();
|
||||
total = Number(row.count) || 0;
|
||||
|
@ -1290,20 +1372,13 @@
|
|||
view: VIEW_NAMES.search,
|
||||
}));
|
||||
}
|
||||
|
||||
const searchParams = [pageSize, offset];
|
||||
const rowsQuery = effectiveSearchBuilder({ matchExpr, pageSize, offset });
|
||||
logDebug('[Search] row SQL', {
|
||||
matchExpr,
|
||||
sql: effectiveSearchSql.trim(),
|
||||
params: searchParams,
|
||||
sql: formatSql(rowsQuery.sql),
|
||||
params: formatParamsForLog(rowsQuery.params),
|
||||
});
|
||||
const searchStmt = prepareForView(
|
||||
db,
|
||||
VIEW_NAMES.search,
|
||||
applyFtsMatch(effectiveSearchSql, matchExpr),
|
||||
'rows',
|
||||
);
|
||||
searchStmt.bind(searchParams);
|
||||
const searchStmt = prepareForView(db, VIEW_NAMES.search, rowsQuery, 'rows');
|
||||
const nextRows = [];
|
||||
while (searchStmt.step()) {
|
||||
nextRows.push(searchStmt.getAsObject());
|
||||
|
@ -1445,31 +1520,34 @@
|
|||
});
|
||||
}
|
||||
|
||||
function escapeLike(str) {
|
||||
return String(str).replace(/[\\%_]/g, (m) => `\\${m}`);
|
||||
}
|
||||
|
||||
function buildLikeTerm() {
|
||||
const typed = state.filter.trim();
|
||||
if (typed) return `%${escapeLike(typed)}%`;
|
||||
if (state.prefix) return `${escapeLike(state.prefix)}%`;
|
||||
if (typed) return `%${typed}%`;
|
||||
if (state.prefix) return `${state.prefix}%`;
|
||||
return '%';
|
||||
}
|
||||
|
||||
const baseCountSql = 'SELECT COUNT(*) AS count FROM artists';
|
||||
const buildBaseRowsSql = (pageSize, offset) => `
|
||||
SELECT id, name
|
||||
const baseCountSql = sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM artists
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT ${pageSize} OFFSET ${offset}
|
||||
`;
|
||||
const countSql = 'SELECT COUNT(*) AS count FROM artists WHERE name LIKE ? ESCAPE "\\"';
|
||||
const buildRowsSql = (likeValue, pageSize, offset) => `
|
||||
const buildBaseRowsSql = (pageSize, offset) => sql`
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE name LIKE ${escapeSqlText(likeValue)} ESCAPE "\\"
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT ${pageSize} OFFSET ${offset}
|
||||
LIMIT ${sqlValue(pageSize)} OFFSET ${sqlValue(offset)}
|
||||
`;
|
||||
const buildFilteredCountSql = (likeValue) => sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM artists
|
||||
WHERE name LIKE ${sqlLike(likeValue)} ESCAPE '\\'
|
||||
`;
|
||||
const buildFilteredRowsSql = (likeValue, pageSize, offset) => sql`
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE name LIKE ${sqlLike(likeValue)} ESCAPE '\\'
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT ${sqlValue(pageSize)} OFFSET ${sqlValue(offset)}
|
||||
`;
|
||||
function updateJumpButtons() {
|
||||
jumpButtons.forEach((btn) => {
|
||||
|
@ -1510,13 +1588,13 @@
|
|||
const rows = [];
|
||||
|
||||
try {
|
||||
const countQuery = useUnfilteredQuery ? baseCountSql : buildFilteredCountSql(likeTerm);
|
||||
const countStmt = prepareForView(
|
||||
db,
|
||||
VIEW_NAMES.browseArtists,
|
||||
useUnfilteredQuery ? baseCountSql : countSql,
|
||||
countQuery,
|
||||
useUnfilteredQuery ? 'count' : 'count-filtered',
|
||||
);
|
||||
if (!useUnfilteredQuery) countStmt.bind([likeTerm]);
|
||||
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
|
||||
countStmt.free();
|
||||
|
||||
|
@ -1538,12 +1616,13 @@
|
|||
({ pageSize, offset } = sanitized);
|
||||
}
|
||||
|
||||
const rowsQuery = useUnfilteredQuery
|
||||
? buildBaseRowsSql(pageSize, offset)
|
||||
: buildFilteredRowsSql(likeTerm, pageSize, offset);
|
||||
const rowsStmt = prepareForView(
|
||||
db,
|
||||
VIEW_NAMES.browseArtists,
|
||||
useUnfilteredQuery
|
||||
? buildBaseRowsSql(pageSize, offset)
|
||||
: buildRowsSql(likeTerm, pageSize, offset),
|
||||
rowsQuery,
|
||||
useUnfilteredQuery ? 'rows' : 'rows-filtered',
|
||||
);
|
||||
while (rowsStmt.step()) rows.push(rowsStmt.getAsObject());
|
||||
|
@ -1666,13 +1745,17 @@
|
|||
title: 'al.title COLLATE NOCASE, a.name COLLATE NOCASE, COALESCE(al.year, 0)',
|
||||
};
|
||||
|
||||
const countSql = 'SELECT COUNT(*) AS count FROM albums';
|
||||
const baseSql = `
|
||||
const countSql = sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM albums
|
||||
`;
|
||||
|
||||
const buildRowsSql = (orderExpr, pageSize, offset) => sql`
|
||||
SELECT al.id, al.title, al.year, a.name AS artist
|
||||
FROM albums al
|
||||
JOIN artists a ON a.id = al.artist_id
|
||||
ORDER BY %ORDER%
|
||||
LIMIT ? OFFSET ?
|
||||
ORDER BY ${sqlIdentifier(orderExpr, { allowlist: Object.values(orderMap) })}
|
||||
LIMIT ${sqlValue(pageSize)} OFFSET ${sqlValue(offset)}
|
||||
`;
|
||||
|
||||
function loadAlbumsImmediate() {
|
||||
|
@ -1710,13 +1793,13 @@
|
|||
}
|
||||
|
||||
const order = orderMap[state.sort] || orderMap.artist;
|
||||
const rowsQuery = buildRowsSql(order, pageSize, offset);
|
||||
const stmt = prepareForView(
|
||||
db,
|
||||
VIEW_NAMES.browseAlbums,
|
||||
baseSql.replace('%ORDER%', order),
|
||||
rowsQuery,
|
||||
'rows',
|
||||
);
|
||||
stmt.bind([pageSize, offset]);
|
||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
||||
stmt.free();
|
||||
} catch (err) {
|
||||
|
@ -1820,22 +1903,26 @@
|
|||
|
||||
let yearButtons = [];
|
||||
|
||||
const yearsSql = `
|
||||
const yearsSql = sql`
|
||||
SELECT year, COUNT(*) AS cnt
|
||||
FROM tracks
|
||||
WHERE year IS NOT NULL
|
||||
GROUP BY year
|
||||
ORDER BY year
|
||||
`;
|
||||
const tracksCountSql = 'SELECT COUNT(*) AS count FROM tracks WHERE year = ?';
|
||||
const tracksRowsSql = `
|
||||
const tracksCountSql = (year) => sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM tracks
|
||||
WHERE year = ${sqlValue(year)}
|
||||
`;
|
||||
const tracksRowsSql = (year, pageSize, offset) => sql`
|
||||
SELECT t.id, t.title, a.name AS artist, IFNULL(al.title, '') AS album, t.genre
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
LEFT JOIN albums al ON al.id = t.album_id
|
||||
WHERE t.year = ?
|
||||
WHERE t.year = ${sqlValue(year)}
|
||||
ORDER BY a.name COLLATE NOCASE, t.title COLLATE NOCASE
|
||||
LIMIT ? OFFSET ?
|
||||
LIMIT ${sqlValue(pageSize)} OFFSET ${sqlValue(offset)}
|
||||
`;
|
||||
|
||||
function updateYearButtons() {
|
||||
|
@ -1919,10 +2006,9 @@
|
|||
const countStmt = prepareForView(
|
||||
db,
|
||||
VIEW_NAMES.browseYears,
|
||||
tracksCountSql,
|
||||
tracksCountSql(state.selectedYear),
|
||||
'tracks-count',
|
||||
);
|
||||
countStmt.bind([state.selectedYear]);
|
||||
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
|
||||
countStmt.free();
|
||||
|
||||
|
@ -1948,10 +2034,9 @@
|
|||
const stmt = prepareForView(
|
||||
db,
|
||||
VIEW_NAMES.browseYears,
|
||||
tracksRowsSql,
|
||||
tracksRowsSql(state.selectedYear, pageSize, offset),
|
||||
'tracks-rows',
|
||||
);
|
||||
stmt.bind([state.selectedYear, pageSize, offset]);
|
||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
||||
stmt.free();
|
||||
} catch (err) {
|
||||
|
@ -2051,22 +2136,26 @@
|
|||
|
||||
let genreButtons = [];
|
||||
|
||||
const genresSql = `
|
||||
const genresSql = sql`
|
||||
SELECT genre, COUNT(*) AS cnt
|
||||
FROM tracks
|
||||
WHERE genre IS NOT NULL AND genre != ''
|
||||
GROUP BY genre
|
||||
ORDER BY cnt DESC, genre COLLATE NOCASE
|
||||
`;
|
||||
const tracksCountSql = 'SELECT COUNT(*) AS count FROM tracks WHERE genre = ?';
|
||||
const tracksRowsSql = `
|
||||
const tracksCountSql = (genre) => sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM tracks
|
||||
WHERE genre = ${sqlValue(genre)}
|
||||
`;
|
||||
const tracksRowsSql = (genre, pageSize, offset) => sql`
|
||||
SELECT t.id, t.title, a.name AS artist, IFNULL(al.title, '') AS album, t.year
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
LEFT JOIN albums al ON al.id = t.album_id
|
||||
WHERE t.genre = ?
|
||||
WHERE t.genre = ${sqlValue(genre)}
|
||||
ORDER BY a.name COLLATE NOCASE, t.title COLLATE NOCASE
|
||||
LIMIT ? OFFSET ?
|
||||
LIMIT ${sqlValue(pageSize)} OFFSET ${sqlValue(offset)}
|
||||
`;
|
||||
|
||||
function renderGenres() {
|
||||
|
@ -2145,10 +2234,9 @@
|
|||
const countStmt = prepareForView(
|
||||
db,
|
||||
VIEW_NAMES.browseGenres,
|
||||
tracksCountSql,
|
||||
tracksCountSql(state.selectedGenre),
|
||||
'tracks-count',
|
||||
);
|
||||
countStmt.bind([state.selectedGenre]);
|
||||
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
|
||||
countStmt.free();
|
||||
|
||||
|
@ -2174,10 +2262,9 @@
|
|||
const stmt = prepareForView(
|
||||
db,
|
||||
VIEW_NAMES.browseGenres,
|
||||
tracksRowsSql,
|
||||
tracksRowsSql(state.selectedGenre, pageSize, offset),
|
||||
'tracks-rows',
|
||||
);
|
||||
stmt.bind([state.selectedGenre, pageSize, offset]);
|
||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
||||
stmt.free();
|
||||
} catch (err) {
|
||||
|
@ -2507,23 +2594,23 @@
|
|||
|
||||
let artistInfo = null;
|
||||
|
||||
const headerSql = `
|
||||
const headerSql = (id) => sql`
|
||||
SELECT a.name,
|
||||
(SELECT COUNT(*) FROM albums WHERE artist_id = a.id) AS album_count,
|
||||
(SELECT COUNT(*) FROM tracks WHERE artist_id = a.id) AS track_count
|
||||
FROM artists a
|
||||
WHERE a.id = ?
|
||||
WHERE a.id = ${sqlValue(id)}
|
||||
`;
|
||||
const albumsSql = `
|
||||
const albumsSql = (id) => sql`
|
||||
SELECT id, title, year
|
||||
FROM albums
|
||||
WHERE artist_id = ?
|
||||
WHERE artist_id = ${sqlValue(id)}
|
||||
ORDER BY COALESCE(year, 0), title COLLATE NOCASE
|
||||
`;
|
||||
const tracksSql = `
|
||||
const tracksSql = (id) => sql`
|
||||
SELECT id, title, year, genre
|
||||
FROM tracks
|
||||
WHERE artist_id = ?
|
||||
WHERE artist_id = ${sqlValue(id)}
|
||||
ORDER BY COALESCE(year, 0), title COLLATE NOCASE
|
||||
LIMIT 100
|
||||
`;
|
||||
|
@ -2567,8 +2654,7 @@
|
|||
|
||||
function loadHeader() {
|
||||
try {
|
||||
const stmt = prepareForView(db, VIEW_NAMES.artistOverlay, headerSql, 'artist-header');
|
||||
stmt.bind([artistId]);
|
||||
const stmt = prepareForView(db, VIEW_NAMES.artistOverlay, headerSql(artistId), 'artist-header');
|
||||
if (stmt.step()) {
|
||||
artistInfo = stmt.getAsObject();
|
||||
$name.textContent = artistInfo.name;
|
||||
|
@ -2591,8 +2677,7 @@
|
|||
loaded.add('albums');
|
||||
try {
|
||||
const rows = [];
|
||||
const stmt = prepareForView(db, VIEW_NAMES.artistOverlay, albumsSql, 'artist-albums');
|
||||
stmt.bind([artistId]);
|
||||
const stmt = prepareForView(db, VIEW_NAMES.artistOverlay, albumsSql(artistId), 'artist-albums');
|
||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
||||
stmt.free();
|
||||
albumTable.setRows(rows);
|
||||
|
@ -2609,8 +2694,7 @@
|
|||
loaded.add('tracks');
|
||||
try {
|
||||
const rows = [];
|
||||
const stmt = prepareForView(db, VIEW_NAMES.artistOverlay, tracksSql, 'artist-tracks');
|
||||
stmt.bind([artistId]);
|
||||
const stmt = prepareForView(db, VIEW_NAMES.artistOverlay, tracksSql(artistId), 'artist-tracks');
|
||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
||||
stmt.free();
|
||||
trackTable.setRows(rows);
|
||||
|
@ -2684,16 +2768,16 @@
|
|||
const $close = el.querySelector('[data-action="close"]');
|
||||
const $tracks = el.querySelector('[data-ref="tracks"]');
|
||||
|
||||
const headerSql = `
|
||||
const headerSql = (id) => sql`
|
||||
SELECT al.title, al.year, a.name AS artist, a.id AS artist_id
|
||||
FROM albums al
|
||||
JOIN artists a ON a.id = al.artist_id
|
||||
WHERE al.id = ?
|
||||
WHERE al.id = ${sqlValue(id)}
|
||||
`;
|
||||
const tracksSql = `
|
||||
const tracksSql = (id) => sql`
|
||||
SELECT id, track_no, title, duration_sec, bitrate_kbps
|
||||
FROM tracks
|
||||
WHERE album_id = ?
|
||||
WHERE album_id = ${sqlValue(id)}
|
||||
ORDER BY COALESCE(track_no, 999), title COLLATE NOCASE
|
||||
`;
|
||||
|
||||
|
@ -2720,8 +2804,7 @@
|
|||
|
||||
function loadHeader() {
|
||||
try {
|
||||
const stmt = prepareForView(db, VIEW_NAMES.albumOverlay, headerSql, 'album-header');
|
||||
stmt.bind([albumId]);
|
||||
const stmt = prepareForView(db, VIEW_NAMES.albumOverlay, headerSql(albumId), 'album-header');
|
||||
if (stmt.step()) {
|
||||
const info = stmt.getAsObject();
|
||||
artistId = Number(info.artist_id);
|
||||
|
@ -2744,8 +2827,7 @@
|
|||
function loadTracks() {
|
||||
try {
|
||||
const rows = [];
|
||||
const stmt = prepareForView(db, VIEW_NAMES.albumOverlay, tracksSql, 'album-tracks');
|
||||
stmt.bind([albumId]);
|
||||
const stmt = prepareForView(db, VIEW_NAMES.albumOverlay, tracksSql(albumId), 'album-tracks');
|
||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
||||
stmt.free();
|
||||
trackTable.setRows(rows);
|
||||
|
@ -2816,7 +2898,7 @@
|
|||
const $close = el.querySelector('[data-action="close"]');
|
||||
const $copy = el.querySelector('[data-action="copy-path"]');
|
||||
|
||||
const sql = `
|
||||
const trackSql = (id) => sql`
|
||||
SELECT t.id, t.title, t.year, t.genre, t.duration_sec, t.bitrate_kbps, t.samplerate_hz,
|
||||
t.channels, t.filesize_bytes, t.sha1, t.relpath,
|
||||
a.id AS artist_id, a.name AS artist,
|
||||
|
@ -2824,15 +2906,14 @@
|
|||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
LEFT JOIN albums al ON al.id = t.album_id
|
||||
WHERE t.id = ?
|
||||
WHERE t.id = ${sqlValue(id)}
|
||||
`;
|
||||
|
||||
let track = null;
|
||||
|
||||
function loadTrack() {
|
||||
try {
|
||||
const stmt = prepareForView(db, VIEW_NAMES.trackOverlay, sql, 'track-detail');
|
||||
stmt.bind([trackId]);
|
||||
const stmt = prepareForView(db, VIEW_NAMES.trackOverlay, trackSql(trackId), 'track-detail');
|
||||
if (stmt.step()) {
|
||||
track = stmt.getAsObject();
|
||||
$title.textContent = track.title || 'Untitled track';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue