(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-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 => 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: `${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 }; });