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

@ -1,8 +1,12 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const Database = require('better-sqlite3');
const {
normalizeChannelName,
projectCatalog,
markProjectionBuilding,
projectionState,
rebuildWithSignals,
serializeChannels,
startRebuild
} = require('../server/services/liveProjectionService');
@ -20,6 +24,64 @@ const item = (id, name, extra = {}) => ({
...extra
});
function createProjectionDb() {
const db = new Database(':memory:');
db.exec(`
CREATE TABLE categories (
id TEXT PRIMARY KEY,
source_id INTEGER NOT NULL,
category_id TEXT NOT NULL,
type TEXT NOT NULL,
name TEXT NOT NULL,
parent_id TEXT,
is_hidden INTEGER DEFAULT 0,
data JSON
);
CREATE TABLE playlist_items (
id TEXT PRIMARY KEY,
source_id INTEGER NOT NULL,
item_id TEXT NOT NULL,
type TEXT NOT NULL,
name TEXT NOT NULL,
category_id TEXT,
parent_id TEXT,
stream_icon TEXT,
stream_url TEXT,
container_extension TEXT,
rating REAL,
year TEXT,
added_at TEXT,
is_hidden INTEGER DEFAULT 0,
is_favorite INTEGER DEFAULT 0,
data JSON
);
`);
const { ensureColumns } = require('../server/db/liveProjectionSchema');
ensureColumns(db, 'playlist_items', LIVE_PROJECTION_COLUMNS);
db.exec(LIVE_PROJECTION_SCHEMA_SQL);
return db;
}
function seedProjectionChannel(db, itemId, name) {
db.prepare(`
INSERT INTO playlist_items
(id, source_id, item_id, type, name, category_id, stream_icon, stream_url, data)
VALUES (?, 1, ?, 'live', ?, 'sports', '', ?, '{}')
`).run(`1:${itemId}`, itemId, name, `https://streams.test/${itemId}.m3u8`);
}
function waitFor(condition, message) {
return new Promise((resolve, reject) => {
const started = Date.now();
const poll = () => {
if (condition()) return resolve();
if (Date.now() - started > 1000) return reject(new Error(message));
setTimeout(poll, 5);
};
poll();
});
}
test('projection migration declares grouping and language columns', () => {
assert.deepEqual(Object.keys(LIVE_PROJECTION_COLUMNS), [
'logical_channel_id', 'grouping_fingerprint', 'grouping_input_hash',
@ -51,6 +113,55 @@ test('manual rebuild scheduling coalesces concurrent requests', async () => {
assert.equal(runs, 1);
});
test('Laya rebuild progress advances after a successful identity batch while analysis is still running', async () => {
const db = createProjectionDb();
for (let index = 0; index < 12; index++) {
seedProjectionChannel(db, `bein-${index}`, `beIN Sports 1 regional ${index}`);
}
let calls = 0;
let secondBatchStarted = false;
let releaseSecondBatch;
const secondBatchGate = new Promise(resolve => { releaseSecondBatch = resolve; });
const layaClient = {
identityHintBatch: async pairs => {
calls += 1;
if (calls === 2) {
secondBatchStarted = true;
await secondBatchGate;
}
return pairs.map(pair => ({
id: pair.id,
sameIdentityProbability: 0.9,
confidence: 0.9,
family: 'bein sports',
number: '1'
}));
}
};
markProjectionBuilding(db);
const rebuild = rebuildWithSignals(db, {
mode: 'laya',
layaClient,
stateAlreadyBuilding: true
});
try {
await waitFor(() => secondBatchStarted, 'Expected the second Laya identity batch to start');
const state = projectionState(db);
assert.equal(state.status, 'building');
assert.equal(state.phase, 'laya_identity');
assert.equal(state.total_count, 12);
assert.equal(state.processed_count, 0);
assert.equal(state.laya_processed_count, 1);
assert.equal(state.laya_total_count, 2);
} finally {
releaseSecondBatch();
await rebuild;
}
});
test('projects beIN quality variants into one logical channel', () => {
const groups = projectCatalog([
item('uhd', '|FR| BEIN SPORTS 1 UHD'),