fix: show live Laya analysis progress (#21)

Track the Laya identity phase separately from catalog projection progress and expose phase-specific counters through the live status API and Settings presentation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
blomios
2026-10-05 23:30:35 +02:00
parent 1abc56c90e
commit 45b56d872f
8 changed files with 281 additions and 22 deletions

View File

@ -141,21 +141,26 @@ function identityCandidatePairs(items) {
return pairs;
}
async function collectIdentityHints(items, mode, layaClient) {
async function collectIdentityHints(items, mode, layaClient, options = {}) {
if (mode === 'algo' || !layaClient) return new Map();
const pairs = identityCandidatePairs(items);
const total = Math.ceil(pairs.length / 64);
let processed = 0;
const hints = new Map();
options.onProgress?.({ processed, total });
for (let index = 0; index < pairs.length; index += 64) {
const results = await layaClient.identityHintBatch(pairs.slice(index, index + 64));
results.forEach(result => hints.set(result.id, result));
processed += 1;
options.onProgress?.({ processed, total });
}
return hints;
}
async function collectOptionalIdentityHints(items, mode, layaClient) {
async function collectOptionalIdentityHints(items, mode, layaClient, options = {}) {
if (mode === 'algo') return new Map();
try {
return await collectIdentityHints(items, mode, layaClient);
return await collectIdentityHints(items, mode, layaClient, options);
} catch (error) {
console.warn('[LiveProjection] Laya identity hints unavailable; continuing deterministically');
return new Map();
@ -177,20 +182,33 @@ function projectionState(database = null) {
database ||= defaultDatabase();
return database.prepare('SELECT * FROM catalog_projection_state WHERE projection = ?').get('live') || {
projection: 'live', status: 'missing', started_at: null, completed_at: null,
input_hash: null, projected_count: 0, processed_count: 0, total_count: 0, error: null
input_hash: null, projected_count: 0, phase: 'idle', processed_count: 0, total_count: 0,
laya_processed_count: 0, laya_total_count: 0, error: null
};
}
function updateProjectionProgress(database, phase, progress = {}) {
database.prepare(`
UPDATE catalog_projection_state SET phase = ?,
laya_processed_count = COALESCE(?, laya_processed_count),
laya_total_count = COALESCE(?, laya_total_count)
WHERE projection = 'live'
`).run(phase, progress.layaProcessed ?? null, progress.layaTotal ?? null);
}
function markProjectionBuilding(database = null) {
database ||= defaultDatabase();
const total = database.prepare("SELECT COUNT(*) AS count FROM playlist_items WHERE type = 'live'").get().count;
const startedAt = Date.now();
database.prepare(`
INSERT INTO catalog_projection_state
(projection, status, started_at, completed_at, input_hash, projected_count, processed_count, total_count, error)
VALUES ('live', 'building', ?, NULL, NULL, 0, 0, ?, NULL)
(projection, status, started_at, completed_at, input_hash, projected_count, phase,
processed_count, total_count, laya_processed_count, laya_total_count, error)
VALUES ('live', 'building', ?, NULL, NULL, 0, 'preparing', 0, ?, 0, 0, NULL)
ON CONFLICT(projection) DO UPDATE SET status = 'building', started_at = excluded.started_at,
completed_at = NULL, processed_count = 0, total_count = excluded.total_count, error = NULL
completed_at = NULL, phase = 'preparing', processed_count = 0,
total_count = excluded.total_count, laya_processed_count = 0,
laya_total_count = 0, error = NULL
`).run(startedAt, total);
return projectionState(database);
}
@ -212,9 +230,11 @@ function loadProjectionItems(database) {
function rebuild(database = null, options = {}) {
database ||= defaultDatabase();
const items = options.items || loadProjectionItems(database);
if (!options.stateAlreadyBuilding) markProjectionBuilding(database);
updateProjectionProgress(database, 'projecting');
const groups = projectCatalog(items, { mode: options.mode, identityHints: options.identityHints });
const inputHash = catalogInputHash(items);
if (!options.stateAlreadyBuilding) markProjectionBuilding(database);
updateProjectionProgress(database, 'writing');
try {
const apply = database.transaction(() => {
@ -243,7 +263,7 @@ function rebuild(database = null, options = {}) {
}
}
database.prepare(`
UPDATE catalog_projection_state SET status = 'success', completed_at = ?,
UPDATE catalog_projection_state SET status = 'success', phase = 'complete', completed_at = ?,
input_hash = ?, projected_count = ?, processed_count = ?, total_count = ?, error = NULL
WHERE projection = 'live'
`).run(Date.now(), inputHash, groups.length, items.length, items.length);
@ -254,7 +274,7 @@ function rebuild(database = null, options = {}) {
const diagnosticCode = /^SQLITE_[A-Z_]+$/.test(error.code || '') ? error.code : 'INTERNAL';
console.error(`[LiveProjection] Rebuild transaction failed (${diagnosticCode}, ${error.name || 'Error'})`);
database.prepare(`
UPDATE catalog_projection_state SET status = 'error', completed_at = ?, error = ?
UPDATE catalog_projection_state SET status = 'error', phase = 'failed', completed_at = ?, error = ?
WHERE projection = 'live'
`).run(Date.now(), `Live catalog projection failed [${diagnosticCode}]`);
throw error;
@ -269,7 +289,12 @@ async function rebuildWithSignals(database = null, options = {}) {
let identityHints = new Map();
if (mode !== 'algo') {
const layaClient = options.layaClient || new (require('./layaService').LayaService)();
identityHints = await collectOptionalIdentityHints(items, mode, layaClient);
identityHints = await collectOptionalIdentityHints(items, mode, layaClient, {
onProgress: ({ processed, total }) => updateProjectionProgress(database, 'laya_identity', {
layaProcessed: processed,
layaTotal: total
})
});
}
return rebuild(database, { ...options, items, mode, identityHints });
}