Use precomputed stats cache in UI
This commit is contained in:
parent
d462b67758
commit
3374b76838
1 changed files with 153 additions and 46 deletions
135
script.js
135
script.js
|
@ -1132,6 +1132,13 @@
|
|||
return '%';
|
||||
}
|
||||
|
||||
const baseCountSql = 'SELECT COUNT(*) AS count FROM artists';
|
||||
const baseRowsSql = `
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
const countSql = 'SELECT COUNT(*) AS count FROM artists WHERE name LIKE ? ESCAPE "\\"';
|
||||
const rowsSql = `
|
||||
SELECT id, name
|
||||
|
@ -1185,6 +1192,7 @@
|
|||
const typedFilter = state.filter.trim();
|
||||
const ftsMatch = buildArtistFtsMatch(typedFilter);
|
||||
const likeTerm = buildLikeTerm();
|
||||
const useUnfilteredQuery = !typedFilter && !state.prefix;
|
||||
listState.showLoading('Loading…');
|
||||
|
||||
const offset = (state.page - 1) * state.pageSize;
|
||||
|
@ -1209,8 +1217,8 @@
|
|||
}
|
||||
|
||||
if (!usedFts) {
|
||||
const countStmt = db.prepare(countSql);
|
||||
countStmt.bind([likeTerm]);
|
||||
const countStmt = db.prepare(useUnfilteredQuery ? baseCountSql : countSql);
|
||||
if (!useUnfilteredQuery) countStmt.bind([likeTerm]);
|
||||
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
|
||||
countStmt.free();
|
||||
|
||||
|
@ -1222,8 +1230,9 @@
|
|||
|
||||
if (offset >= total) state.page = Math.max(1, Math.ceil(total / state.pageSize));
|
||||
|
||||
const rowsStmt = db.prepare(rowsSql);
|
||||
rowsStmt.bind([likeTerm, state.pageSize, (state.page - 1) * state.pageSize]);
|
||||
const rowsStmt = db.prepare(useUnfilteredQuery ? baseRowsSql : rowsSql);
|
||||
if (useUnfilteredQuery) rowsStmt.bind([state.pageSize, (state.page - 1) * state.pageSize]);
|
||||
else rowsStmt.bind([likeTerm, state.pageSize, (state.page - 1) * state.pageSize]);
|
||||
while (rowsStmt.step()) rows.push(rowsStmt.getAsObject());
|
||||
rowsStmt.free();
|
||||
}
|
||||
|
@ -1829,6 +1838,7 @@
|
|||
const $cards = el.querySelector('[data-ref="cards"]');
|
||||
const $lists = el.querySelector('[data-ref="lists"]');
|
||||
|
||||
const siteStatsSql = 'SELECT name, value FROM site_stats';
|
||||
const totalsSql = `
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM artists) AS artists,
|
||||
|
@ -1860,19 +1870,82 @@
|
|||
LIMIT 10
|
||||
`;
|
||||
|
||||
function loadPrecomputedStats() {
|
||||
try {
|
||||
const stmt = db.prepare(siteStatsSql);
|
||||
const map = new Map();
|
||||
try {
|
||||
while (stmt.step()) {
|
||||
const row = stmt.getAsObject();
|
||||
map.set(String(row.name), String(row.value));
|
||||
}
|
||||
} finally {
|
||||
stmt.free();
|
||||
}
|
||||
return map.size ? map : null;
|
||||
} catch (err) {
|
||||
console.warn('Failed to load precomputed stats', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function collectOrderedStats(statsMap, prefix, transform) {
|
||||
if (!statsMap) return null;
|
||||
const items = [];
|
||||
statsMap.forEach((rawValue, name) => {
|
||||
if (!name.startsWith(prefix)) return;
|
||||
const rank = Number(name.slice(prefix.length));
|
||||
if (!Number.isFinite(rank) || rank <= 0) return;
|
||||
let value;
|
||||
try {
|
||||
value = transform(rawValue);
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse stat', name, err);
|
||||
value = null;
|
||||
}
|
||||
if (value) items.push({ rank, value });
|
||||
});
|
||||
if (!items.length) return null;
|
||||
items.sort((a, b) => a.rank - b.rank);
|
||||
return items.map((item) => item.value);
|
||||
}
|
||||
|
||||
const siteStats = loadPrecomputedStats();
|
||||
|
||||
function renderTotals() {
|
||||
const hasPrecomputedCounts = siteStats
|
||||
&& siteStats.has('count.artists')
|
||||
&& siteStats.has('count.albums')
|
||||
&& siteStats.has('count.tracks');
|
||||
|
||||
if (hasPrecomputedCounts) {
|
||||
const metrics = [
|
||||
{ label: 'Artists', value: Number(siteStats.get('count.artists')), action: () => navigateTo('browseArtists') },
|
||||
{ label: 'Albums', value: Number(siteStats.get('count.albums')), action: () => navigateTo('browseAlbums') },
|
||||
{ label: 'Tracks', value: Number(siteStats.get('count.tracks')), action: () => navigateTo('search') },
|
||||
];
|
||||
renderMetricCards(metrics);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stmt = db.prepare(totalsSql);
|
||||
stmt.step();
|
||||
const totals = stmt.getAsObject();
|
||||
stmt.free();
|
||||
|
||||
const metrics = [
|
||||
{ label: 'Artists', value: Number(totals.artists), action: () => navigateTo('browseArtists') },
|
||||
{ label: 'Albums', value: Number(totals.albums), action: () => navigateTo('browseAlbums') },
|
||||
{ label: 'Tracks', value: Number(totals.tracks), action: () => navigateTo('search') },
|
||||
];
|
||||
renderMetricCards(metrics);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
$cards.innerHTML = '<div class="column"><div class="notification is-danger">Failed to load stats.</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderMetricCards(metrics) {
|
||||
$cards.innerHTML = '';
|
||||
metrics.forEach((metric) => {
|
||||
const column = document.createElement('div');
|
||||
|
@ -1896,33 +1969,67 @@
|
|||
column.appendChild(box);
|
||||
$cards.appendChild(column);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
$cards.innerHTML = '<div class="column"><div class="notification is-danger">Failed to load stats.</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTopLists() {
|
||||
let topArtists = [];
|
||||
let topYears = [];
|
||||
let topGenres = [];
|
||||
const parseJson = (value) => {
|
||||
if (!value) return null;
|
||||
return JSON.parse(value);
|
||||
};
|
||||
|
||||
const precomputedArtists = collectOrderedStats(siteStats, 'top.artist.', (raw) => {
|
||||
const data = parseJson(raw);
|
||||
if (!data) return null;
|
||||
return {
|
||||
artist_id: Number(data.artist_id),
|
||||
artist: data.name,
|
||||
cnt: Number(data.tracks),
|
||||
};
|
||||
});
|
||||
const precomputedYears = collectOrderedStats(siteStats, 'top.year.', (raw) => {
|
||||
const data = parseJson(raw);
|
||||
if (!data) return null;
|
||||
return {
|
||||
year: Number(data.year),
|
||||
cnt: Number(data.tracks),
|
||||
};
|
||||
});
|
||||
const precomputedGenres = collectOrderedStats(siteStats, 'top.genre.', (raw) => {
|
||||
const data = parseJson(raw);
|
||||
if (!data) return null;
|
||||
return {
|
||||
genre: data.genre,
|
||||
cnt: Number(data.tracks),
|
||||
};
|
||||
});
|
||||
|
||||
let topArtists = precomputedArtists || [];
|
||||
let topYears = precomputedYears || [];
|
||||
let topGenres = precomputedGenres || [];
|
||||
|
||||
if (!precomputedArtists || !precomputedYears || !precomputedGenres) {
|
||||
try {
|
||||
if (!precomputedArtists) {
|
||||
const artistStmt = db.prepare(topArtistsSql);
|
||||
while (artistStmt.step()) topArtists.push(artistStmt.getAsObject());
|
||||
artistStmt.free();
|
||||
|
||||
}
|
||||
if (!precomputedYears) {
|
||||
const yearStmt = db.prepare(topYearsSql);
|
||||
while (yearStmt.step()) topYears.push(yearStmt.getAsObject());
|
||||
yearStmt.free();
|
||||
|
||||
}
|
||||
if (!precomputedGenres) {
|
||||
const genreStmt = db.prepare(topGenresSql);
|
||||
while (genreStmt.step()) topGenres.push(genreStmt.getAsObject());
|
||||
genreStmt.free();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
$lists.innerHTML = '<p class="has-text-danger">Failed to load ranking lists.</p>';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$lists.innerHTML = '';
|
||||
const columns = document.createElement('div');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue