diff --git a/script.js b/script.js index 67c3fc6..80e8b93 100644 --- a/script.js +++ b/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,68 +1870,165 @@ 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') }, ]; - - $cards.innerHTML = ''; - metrics.forEach((metric) => { - const column = document.createElement('div'); - column.className = 'column'; - const box = document.createElement('div'); - box.className = 'box has-text-centered'; - const value = document.createElement('p'); - value.className = 'title is-3'; - value.textContent = formatNumber(metric.value); - const label = document.createElement('p'); - label.className = 'subtitle is-6'; - label.textContent = metric.label; - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'button is-small is-link'; - button.textContent = `Open ${metric.label}`; - button.addEventListener('click', metric.action); - box.appendChild(value); - box.appendChild(label); - box.appendChild(button); - column.appendChild(box); - $cards.appendChild(column); - }); + renderMetricCards(metrics); } catch (err) { console.error(err); $cards.innerHTML = '
Failed to load ranking lists.
'; - return; + 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 = 'Failed to load ranking lists.
'; + return; + } } $lists.innerHTML = '';