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:
blomios
2026-10-02 15:35:02 +02:00
parent 7b6525708c
commit 9f97020996
16 changed files with 619 additions and 10 deletions

View File

@ -35,6 +35,7 @@ class ChannelList {
this.languageDetectionNotice = '';
this.languageDetectionFallback = false;
this._languageDetectionRequest = 0;
this.projectionNotice = '';
this.loadCollapsedState();
this.init();
@ -436,6 +437,13 @@ class ChannelList {
this.currentBatch = 0;
this.batchSize = 100; // Number of groups to render per batch (increased to handle many hidden groups)
this.container.innerHTML = ''; // Clear container
if (this.projectionNotice) {
const notice = document.createElement('div');
notice.className = 'live-projection-notice';
notice.setAttribute('role', 'status');
notice.textContent = this.projectionNotice;
this.container.appendChild(notice);
}
if (this.languageDetectionNotice) {
const notice = document.createElement('div');
notice.className = 'laya-language-notice';
@ -913,6 +921,12 @@ class ChannelList {
try {
this.container.innerHTML = '<div class="loading"></div>';
if (await this.loadProjectedChannels(parseInt(id, 10))) {
this.render();
void this.refreshLayaLanguages();
return;
}
if (type === 'xtream') {
await this.loadXtreamChannels(parseInt(id));
} else if (type === 'm3u') {
@ -945,6 +959,12 @@ class ChannelList {
try {
this.container.innerHTML = '<div class="loading"></div>';
if (await this.loadProjectedChannels()) {
this.render();
void this.refreshLayaLanguages();
return;
}
const xtreamSources = this.sources.filter(s => s.type === 'xtream' && s.enabled);
const m3uSources = this.sources.filter(s => s.type === 'm3u' && s.enabled);
console.log('[ChannelList] loadAllChannels: xtream=', xtreamSources.length, 'm3u=', m3uSources.length);
@ -968,6 +988,26 @@ class ChannelList {
}
}
async loadProjectedChannels(sourceId = null) {
try {
const response = await API.live.getChannels({ sourceId, includeHidden: true });
if (!LiveChannelProjection.canUse(response)) {
this.projectionNotice = 'Channel list is updating. Using the standard channel list for now.';
return false;
}
const mapped = LiveChannelProjection.mapResponse(response, this.sources);
this.channels = mapped.channels;
this.visibleFavorites = mapped.favorites;
this.hiddenItems = mapped.hiddenItems;
this.projectionNotice = '';
return true;
} catch (err) {
console.warn('[ChannelList] Live projection unavailable; using legacy loading:', err.message);
this.projectionNotice = '';
return false;
}
}
/**
* Load Xtream channels
*/