Add language filter controls for projected live channels, preserve backend language views in the browser, and separate built-in EPG sources from custom sources. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
94 lines
4.7 KiB
JavaScript
94 lines
4.7 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 = [];
|
|
const logicalChannels = [];
|
|
const hasLanguageViews = response.channels.every(channel => channel?.language && channel?.logicalChannelId);
|
|
|
|
response.channels.forEach(logicalChannel => {
|
|
if (!logicalChannel || !Array.isArray(logicalChannel.variants)) return;
|
|
const mappedVariants = [];
|
|
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 ?? ''
|
|
};
|
|
if (variant.language || logicalChannel.language) {
|
|
mapped.language = variant.language || logicalChannel.language;
|
|
}
|
|
channels.push(mapped);
|
|
mappedVariants.push(mapped);
|
|
const key = `${mapped.sourceId}:${id}`;
|
|
if (variant.isFavorite) favorites.add(key);
|
|
if (variant.isHidden) hiddenItems.add(`channel:${mapped.sourceId}:${mapped.streamId || id}`);
|
|
});
|
|
if (!mappedVariants.length) return;
|
|
const preferredVariant = mappedVariants.find(variant =>
|
|
`${variant.sourceId}:${variant.id}` === String(logicalChannel.selectedVariantId)
|
|
) || mappedVariants.find(variant => String(variant.id) === String(logicalChannel.preferredVariant?.id))
|
|
|| mappedVariants[0];
|
|
const language = logicalChannel.language || preferredVariant.language;
|
|
if (hasLanguageViews) logicalChannels.push({
|
|
...logicalChannel,
|
|
fingerprint: logicalChannel.id,
|
|
displayName: logicalChannel.displayName || logicalChannel.standardName || logicalChannel.name,
|
|
name: logicalChannel.displayName || logicalChannel.standardName || logicalChannel.name,
|
|
variants: mappedVariants,
|
|
visibleVariants: mappedVariants.filter(variant => !variant.isHidden),
|
|
preferredVariant,
|
|
language,
|
|
selectedVariantId: `${preferredVariant.sourceId}:${preferredVariant.id}`,
|
|
groupTitle: preferredVariant.groupTitle,
|
|
tvgLogo: preferredVariant.tvgLogo,
|
|
tvgId: logicalChannel.epg?.channelId || preferredVariant.tvgId,
|
|
isFavorite: mappedVariants.some(variant => variant.isFavorite),
|
|
isHidden: mappedVariants.every(variant => variant.isHidden)
|
|
});
|
|
});
|
|
|
|
return {
|
|
channels, logicalChannels: hasLanguageViews ? logicalChannels : null, favorites, hiddenItems,
|
|
availableLanguages: response.filters?.availableLanguages || [],
|
|
projection: response.projection || {}
|
|
};
|
|
}
|
|
|
|
function canUse(response) {
|
|
if (!response || !Array.isArray(response.channels) || response.projection?.stale) return false;
|
|
|
|
const projection = response.projection || {};
|
|
const expectedChannelCount = [
|
|
projection.total,
|
|
projection.processed,
|
|
projection.catalogCount,
|
|
projection.sourceChannelCount
|
|
].some(count => Number(count) > 0);
|
|
|
|
return response.channels.length > 0 || !expectedChannelCount;
|
|
}
|
|
|
|
return { canUse, mapResponse };
|
|
});
|