/** * EPG Matching Service * Matches EPG channels to our Channel objects using tvgId */ import type { Channel, EPGChannel, EPGProgram } from '../types'; /** * Match EPG channels to our Channel objects * Returns a map of channelId -> EPGProgram[] */ export function matchEPGToChannels( epgChannels: EPGChannel[], epgPrograms: EPGProgram[], channels: Channel[] ): Map { // Create mapping: channelId -> EPGProgram[] const channelEPGMap = new Map(); // Build EPG channel ID -> our channel IDs mapping const epgChannelMap = new Map(); // First pass: Match EPG channels to our channels for (const epgChannel of epgChannels) { const matchingChannels: Channel[] = []; // Strategy 1: Match by exact tvgId const exactMatches = channels.filter( (ch) => ch.tvgId === epgChannel.id ); matchingChannels.push(...exactMatches); // Strategy 2: Match by tvgName (case-insensitive) if (matchingChannels.length === 0) { const nameMatches = channels.filter( (ch) => ch.tvgName?.toLowerCase() === epgChannel.displayName.toLowerCase() ); matchingChannels.push(...nameMatches); } // Strategy 3: Match by channel name (fuzzy) if (matchingChannels.length === 0) { const fuzzyMatches = channels.filter( (ch) => ch.name.toLowerCase().includes(epgChannel.displayName.toLowerCase()) || epgChannel.displayName.toLowerCase().includes(ch.name.toLowerCase()) ); matchingChannels.push(...fuzzyMatches); } if (matchingChannels.length > 0) { epgChannelMap.set( epgChannel.id, matchingChannels.map((ch) => ch.id) ); // Update EPG channel with matched channel IDs epgChannel.channelIds = matchingChannels.map((ch) => ch.id); } } // Second pass: Map programs to channels for (const program of epgPrograms) { const channelIds = epgChannelMap.get(program.channelId); if (channelIds) { for (const channelId of channelIds) { if (!channelEPGMap.has(channelId)) { channelEPGMap.set(channelId, []); } channelEPGMap.get(channelId)!.push({ ...program, channelId, // Use our channel ID }); } } } // Sort programs by start time for each channel for (const [, programs] of channelEPGMap.entries()) { programs.sort( (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime() ); } return channelEPGMap; } /** * Get current program for a channel */ export function getCurrentProgram( programs: EPGProgram[], now: Date = new Date() ): EPGProgram | null { const nowTime = now.getTime(); return ( programs.find((program) => { const start = new Date(program.start).getTime(); const end = new Date(program.end).getTime(); return nowTime >= start && nowTime < end; }) || null ); } /** * Get next program for a channel */ export function getNextProgram( programs: EPGProgram[], now: Date = new Date() ): EPGProgram | null { const nowTime = now.getTime(); return ( programs.find((program) => { const start = new Date(program.start).getTime(); return start > nowTime; }) || null ); } /** * Get programs for a time range */ export function getProgramsInRange( programs: EPGProgram[], startTime: Date, endTime: Date ): EPGProgram[] { const start = startTime.getTime(); const end = endTime.getTime(); return programs.filter((program) => { const programStart = new Date(program.start).getTime(); const programEnd = new Date(program.end).getTime(); // Overlap check: program overlaps with requested range return programStart < end && programEnd > start; }); } /** * Get programs for a specific date */ export function getProgramsForDate( programs: EPGProgram[], date: Date ): EPGProgram[] { const startOfDay = new Date(date); startOfDay.setHours(0, 0, 0, 0); const endOfDay = new Date(date); endOfDay.setHours(23, 59, 59, 999); return getProgramsInRange(programs, startOfDay, endOfDay); } /** * Search programs by title or description */ export function searchPrograms( programs: EPGProgram[], query: string ): EPGProgram[] { const lowerQuery = query.toLowerCase(); return programs.filter((program) => { const titleMatch = program.title.toLowerCase().includes(lowerQuery); const descMatch = program.description?.toLowerCase().includes(lowerQuery); return titleMatch || descMatch; }); }