Persist live channel language metadata across background projection rebuilds and target Local AI identity analysis to newly detected streams unless a manual full analysis is requested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
5.4 KiB
JavaScript
172 lines
5.4 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const Database = require('better-sqlite3');
|
|
const {
|
|
ensureColumns,
|
|
LIVE_PROJECTION_COLUMNS,
|
|
LIVE_PROJECTION_SCHEMA_SQL
|
|
} = require('../server/db/liveProjectionSchema');
|
|
const liveProjection = require('../server/services/liveProjectionService');
|
|
|
|
function createDb() {
|
|
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
|
|
);
|
|
CREATE TABLE favorites (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
source_id INTEGER NOT NULL,
|
|
item_id TEXT NOT NULL,
|
|
item_type TEXT NOT NULL
|
|
);
|
|
`);
|
|
ensureColumns(db, 'playlist_items', LIVE_PROJECTION_COLUMNS);
|
|
db.exec(LIVE_PROJECTION_SCHEMA_SQL);
|
|
return db;
|
|
}
|
|
|
|
function seedChannel(db, id, name, extra = {}) {
|
|
db.prepare(`
|
|
INSERT INTO playlist_items
|
|
(id, source_id, item_id, type, name, category_id, stream_icon, stream_url, data,
|
|
language_code, language_confidence, language_detector, language_input_hash)
|
|
VALUES (?, 1, ?, 'live', ?, 'general', '', ?, '{}', ?, ?, ?, ?)
|
|
`).run(
|
|
`1:${id}`,
|
|
id,
|
|
name,
|
|
`https://streams.test/${id}.m3u8`,
|
|
extra.languageCode || null,
|
|
extra.languageConfidence || null,
|
|
extra.languageDetector || null,
|
|
extra.languageInputHash || null
|
|
);
|
|
}
|
|
|
|
test('background projection rebuild keeps a persisted stream language instead of redetecting it', () => {
|
|
const db = createDb();
|
|
seedChannel(db, 'persisted', 'World News', {
|
|
languageCode: 'fr',
|
|
languageConfidence: 0.99,
|
|
languageDetector: 'laya',
|
|
languageInputHash: 'stable-language-input'
|
|
});
|
|
|
|
try {
|
|
liveProjection.rebuild(db, { mode: 'algo' });
|
|
const row = db.prepare(`
|
|
SELECT language_code, language_detector, language_confidence
|
|
FROM playlist_items
|
|
WHERE id = '1:persisted'
|
|
`).get();
|
|
|
|
assert.equal(row.language_code, 'fr');
|
|
assert.equal(row.language_detector, 'laya');
|
|
assert.equal(Number(row.language_confidence), 0.99);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
test('Laya analysis for one newly detected stream does not send old catalog pairs', async () => {
|
|
const db = createDb();
|
|
for (let index = 0; index < 8; index++) {
|
|
seedChannel(db, `old-${index}`, `beIN Sports 1 legacy ${index}`, {
|
|
languageCode: 'fr',
|
|
languageConfidence: 0.95,
|
|
languageDetector: 'laya',
|
|
languageInputHash: `old-${index}-hash`
|
|
});
|
|
}
|
|
seedChannel(db, 'new-stream', 'beIN Sports 1 new feed');
|
|
|
|
const oldCatalogPairs = [];
|
|
const layaClient = {
|
|
identityHintBatch: async pairs => {
|
|
for (const pair of pairs) {
|
|
if (!pair.left.name.includes('new feed') && !pair.right.name.includes('new feed')) {
|
|
oldCatalogPairs.push(pair);
|
|
}
|
|
}
|
|
return pairs.map(pair => ({
|
|
id: pair.id,
|
|
sameIdentityProbability: 0.9,
|
|
confidence: 0.9,
|
|
family: 'bein sports',
|
|
number: '1'
|
|
}));
|
|
}
|
|
};
|
|
|
|
try {
|
|
await liveProjection.rebuildWithSignals(db, { mode: 'laya', layaClient });
|
|
assert.equal(oldCatalogPairs.length, 0, `unexpected old-catalog Laya pairs=${oldCatalogPairs.length}`);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|
|
|
|
test('analyzeAll sends persisted catalog pairs to Laya', async () => {
|
|
const db = createDb();
|
|
seedChannel(db, 'persisted-a', 'beIN Sports 1 east', {
|
|
languageCode: 'fr',
|
|
languageConfidence: 0.95,
|
|
languageDetector: 'laya',
|
|
languageInputHash: 'persisted-a-hash'
|
|
});
|
|
seedChannel(db, 'persisted-b', 'beIN Sports 1 west', {
|
|
languageCode: 'fr',
|
|
languageConfidence: 0.95,
|
|
languageDetector: 'laya',
|
|
languageInputHash: 'persisted-b-hash'
|
|
});
|
|
|
|
let layaPairs = 0;
|
|
const layaClient = {
|
|
identityHintBatch: async pairs => {
|
|
layaPairs += pairs.length;
|
|
return pairs.map(pair => ({
|
|
id: pair.id,
|
|
sameIdentityProbability: 0.9,
|
|
confidence: 0.9,
|
|
family: 'bein sports',
|
|
number: '1'
|
|
}));
|
|
}
|
|
};
|
|
|
|
try {
|
|
await liveProjection.rebuildWithSignals(db, { mode: 'laya', layaClient, analyzeAll: true });
|
|
assert.ok(layaPairs > 0);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
});
|