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>
51 lines
2.4 KiB
JavaScript
51 lines
2.4 KiB
JavaScript
(function (root, factory) {
|
|
const service = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = service;
|
|
if (root) root.LiveChannelProjection = service;
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
function mapResponse(response, sources = []) {
|
|
if (!response || !Array.isArray(response.channels)) {
|
|
throw new Error('Invalid live channel projection response');
|
|
}
|
|
|
|
const sourceById = new Map(sources.map(source => [String(source.id), source]));
|
|
const favorites = new Set();
|
|
const hiddenItems = new Set();
|
|
const channels = [];
|
|
|
|
response.channels.forEach(logicalChannel => {
|
|
if (!logicalChannel || !Array.isArray(logicalChannel.variants)) return;
|
|
logicalChannel.variants.forEach(variant => {
|
|
const source = sourceById.get(String(variant.sourceId));
|
|
const id = String(variant.id);
|
|
const mapped = {
|
|
...variant,
|
|
id,
|
|
logicalId: logicalChannel.id,
|
|
logicalFingerprint: logicalChannel.fingerprint || logicalChannel.id,
|
|
logicalDisplayName: logicalChannel.displayName || logicalChannel.name,
|
|
streamId: variant.streamId ?? id,
|
|
tvgId: variant.tvgId ?? variant.epg_channel_id ?? null,
|
|
tvgLogo: variant.tvgLogo ?? variant.streamIcon ?? variant.stream_icon ?? '',
|
|
url: variant.url ?? variant.streamUrl ?? variant.stream_url ?? '',
|
|
groupTitle: variant.groupTitle ?? variant.categoryName ?? variant.categoryId ?? 'Uncategorized',
|
|
sourceType: variant.sourceType ?? source?.type ?? 'xtream',
|
|
sourceName: variant.sourceName ?? source?.name ?? ''
|
|
};
|
|
channels.push(mapped);
|
|
const key = `${mapped.sourceId}:${id}`;
|
|
if (variant.isFavorite) favorites.add(key);
|
|
if (variant.isHidden) hiddenItems.add(`channel:${mapped.sourceId}:${mapped.streamId || id}`);
|
|
});
|
|
});
|
|
|
|
return { channels, favorites, hiddenItems, projection: response.projection || {} };
|
|
}
|
|
|
|
function canUse(response) {
|
|
return Boolean(response && Array.isArray(response.channels) && !response.projection?.stale);
|
|
}
|
|
|
|
return { canUse, mapResponse };
|
|
});
|