Files
Nodecast/public/js/services/liveChannelProjection.js
blomios a447d0dfe3 fix: restore Laya language analysis and stream variants
Run Local AI language detection during full rebuilds after identity classification and restore variant expansion with a visible playing stream indicator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-08 00:14:10 +02:00

133 lines
6.6 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 normalizeSearch(value) {
return String(value || '')
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/([a-z])([0-9])/g, '$1 $2')
.replace(/([0-9])([a-z])/g, '$1 $2')
.replace(/[^a-z0-9]+/g, ' ').trim()
.split(/\s+/).filter(Boolean)
.map(token => token.length > 3 && token.endsWith('s') ? token.slice(0, -1) : token);
}
function matchesSearch(channel, query) {
const wanted = normalizeSearch(query);
if (!wanted.length) return true;
const corpus = normalizeSearch([
channel.displayName, channel.standardName, channel.fingerprint,
...(channel.variants || []).flatMap(variant => [
variant.name, variant.tvgId, variant.categoryName, variant.groupTitle
])
].filter(Boolean).join(' '));
return wanted.every(token => corpus.some(candidate => {
if (/^\d+$/.test(token)) return /^\d+$/.test(candidate) && Number(candidate) === Number(token);
return candidate.includes(token);
}));
}
function normalizeLanguage(language) {
const rawCode = typeof language?.code === 'string' ? language.code.trim().toLowerCase() : '';
const code = rawCode || 'und';
const confidence = Number(language?.confidence);
return {
code,
short: language?.short || (code === 'und' ? 'UND' : code.toUpperCase()),
label: language?.label || (code === 'und' ? 'Unknown' : code.toUpperCase()),
confidence: Number.isFinite(confidence) ? confidence : 0
};
}
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?.logicalChannelId);
response.channels.forEach(logicalChannel => {
if (!logicalChannel) return;
const rawVariants = Array.isArray(logicalChannel.variants)
? logicalChannel.variants.filter(Boolean)
: (logicalChannel.preferredVariant ? [logicalChannel.preferredVariant] : []);
const mappedVariants = [];
rawVariants.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 ?? ''
};
mapped.language = normalizeLanguage(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 = normalizeLanguage(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: logicalChannel.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 || []).filter(Boolean).map(normalizeLanguage),
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, matchesSearch, normalizeLanguage, normalizeSearch };
});