feat: precompute stream grouping during source sync (#13)
Move variant grouping work out of Live TV rendering into the catalog pipeline: a liveProjectionService builds the logical channel projection (schema in liveProjectionSchema) whenever a source is added, synced or modified (name change, new address), and the projection is served through a /live route. ChannelList now consumes the precomputed projection via a liveChannelProjection client service, recalculating only when the catalog changes. Cover projection building, incremental recompute and client consumption with tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
49
server/db/liveProjectionSchema.js
Normal file
49
server/db/liveProjectionSchema.js
Normal file
@ -0,0 +1,49 @@
|
||||
const LIVE_PROJECTION_COLUMNS = {
|
||||
logical_channel_id: 'TEXT',
|
||||
grouping_fingerprint: 'TEXT',
|
||||
grouping_input_hash: 'TEXT',
|
||||
quality_label: 'TEXT',
|
||||
quality_rank: 'INTEGER',
|
||||
variant_rank: 'INTEGER',
|
||||
language_code: 'TEXT',
|
||||
language_confidence: 'REAL',
|
||||
language_scores: 'JSON',
|
||||
language_evidence: 'JSON',
|
||||
language_detector: 'TEXT',
|
||||
language_input_hash: 'TEXT'
|
||||
};
|
||||
|
||||
const LIVE_PROJECTION_SCHEMA_SQL = `
|
||||
CREATE INDEX IF NOT EXISTS idx_items_logical_channel ON playlist_items(logical_channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_items_grouping_fingerprint ON playlist_items(grouping_fingerprint);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS logical_channels (
|
||||
id TEXT PRIMARY KEY,
|
||||
fingerprint TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
preferred_variant_id TEXT,
|
||||
variant_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_logical_channels_fingerprint ON logical_channels(fingerprint);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS catalog_projection_state (
|
||||
projection TEXT PRIMARY KEY,
|
||||
status TEXT NOT NULL,
|
||||
started_at INTEGER,
|
||||
completed_at INTEGER,
|
||||
input_hash TEXT,
|
||||
projected_count INTEGER NOT NULL DEFAULT 0,
|
||||
error TEXT
|
||||
);
|
||||
`;
|
||||
|
||||
function ensureColumns(database, table, columns) {
|
||||
const existing = new Set(database.prepare(`PRAGMA table_info(${table})`).all().map(column => column.name));
|
||||
for (const [name, definition] of Object.entries(columns)) {
|
||||
if (!existing.has(name)) database.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${definition}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ensureColumns, LIVE_PROJECTION_COLUMNS, LIVE_PROJECTION_SCHEMA_SQL };
|
||||
@ -1,6 +1,11 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const {
|
||||
ensureColumns,
|
||||
LIVE_PROJECTION_COLUMNS,
|
||||
LIVE_PROJECTION_SCHEMA_SQL
|
||||
} = require('./liveProjectionSchema');
|
||||
|
||||
const dataDir = path.join(__dirname, '..', '..', 'data');
|
||||
const dbPath = path.join(dataDir, 'content.db');
|
||||
@ -73,6 +78,9 @@ function initSchema() {
|
||||
CREATE INDEX IF NOT EXISTS idx_items_category ON playlist_items(source_id, category_id);
|
||||
`);
|
||||
|
||||
ensureColumns(db, 'playlist_items', LIVE_PROJECTION_COLUMNS);
|
||||
db.exec(LIVE_PROJECTION_SCHEMA_SQL);
|
||||
|
||||
// EPG Programs
|
||||
// Optimized for range queries
|
||||
db.exec(`
|
||||
@ -247,6 +255,8 @@ const layaLanguageCache = {
|
||||
module.exports = {
|
||||
getDb,
|
||||
initSchema,
|
||||
ensureColumns,
|
||||
LIVE_PROJECTION_COLUMNS,
|
||||
favorites,
|
||||
layaLanguageCache
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user