feat: persist incremental live channel analysis

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>
This commit is contained in:
blomios
2026-10-07 23:12:42 +02:00
parent 9967fd7adf
commit 4319e64e1e
10 changed files with 516 additions and 98 deletions

View File

@ -8,6 +8,18 @@ const liveProjection = require('./liveProjectionService');
// Sync tracking
const activeSyncs = new Set(); // sourceId
async function rebuildLiveProjection(newIds) {
const currentSettings = await settings.get();
const mode = currentSettings.languageDetectionMode;
if (newIds.size > 0 && (mode === 'laya' || mode === 'hybrid')) {
return liveProjection.startRebuild(undefined, {
mode,
scopeItemIds: [...newIds]
}).promise;
}
return liveProjection.queueRebuild();
}
class SyncService {
constructor() {
this.lastSyncTime = null; // Track when global sync last completed
@ -183,8 +195,8 @@ class SyncService {
// 2. Live Streams
console.log(`[Sync] Fetching Live Streams for ${source.name}`);
const liveStreams = await api.getLiveStreams();
await this.saveStreams(source.id, 'live', liveStreams);
await liveProjection.queueRebuild();
const { newIds } = await this.saveStreams(source.id, 'live', liveStreams);
await rebuildLiveProjection(newIds);
// 3. VOD Categories
console.log(`[Sync] Fetching VOD Categories for ${source.name}`);
@ -259,15 +271,17 @@ class SyncService {
* @param {string} type - Type of items (live, movie, series)
* @param {Array} items - Items to save
* @param {Object} options - Options { skipPurge: boolean }
* @returns {Set} Set of synced IDs (for external purge if skipPurge was true)
* @returns {{ syncedIds: Set<string>, newIds: Set<string> }} Synced and newly inserted IDs
*/
async saveStreams(sourceId, type, items, options = {}) {
if (!items || items.length === 0) return new Set();
if (!items || items.length === 0) return { syncedIds: new Set(), newIds: new Set() };
const db = getDb();
const { skipPurge = false } = options;
// Collect all IDs we're syncing
const syncedIds = new Set();
const newIds = new Set();
const existingItem = db.prepare('SELECT 1 FROM playlist_items WHERE id = ?');
const stmt = db.prepare(`
INSERT INTO playlist_items (
@ -316,6 +330,7 @@ class SyncService {
const id = `${sourceId}:${itemId}`;
syncedIds.add(id);
if (!existingItem.get(id)) newIds.add(id);
stmt.run(
id,
@ -349,7 +364,7 @@ class SyncService {
}
console.log(`[Sync] Saved ${items.length} ${type} items`);
return syncedIds;
return { syncedIds, newIds };
}
/**
@ -511,6 +526,7 @@ class SyncService {
const allGroups = new Set();
const allSyncedIds = new Set(); // Collect IDs across all batches
const allNewIds = new Set();
let totalChannels = 0;
let batchCount = 0;
@ -530,8 +546,9 @@ class SyncService {
// Save this batch immediately (skip purge - we'll do it at the end)
if (playlistItems.length > 0) {
const batchIds = await this.saveStreams(source.id, 'live', playlistItems, { skipPurge: true });
batchIds.forEach(id => allSyncedIds.add(id));
const { syncedIds, newIds } = await this.saveStreams(source.id, 'live', playlistItems, { skipPurge: true });
syncedIds.forEach(id => allSyncedIds.add(id));
newIds.forEach(id => allNewIds.add(id));
totalChannels += playlistItems.length;
}
@ -561,7 +578,7 @@ class SyncService {
}));
await this.saveCategories(source.id, 'live', categories);
await liveProjection.queueRebuild();
await rebuildLiveProjection(allNewIds);
console.log(`[Sync] M3U sync complete for ${source.name}`);
}