const API_ROOT = 'https://iptv-org.github.io/api'; function normalize(value) { return String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '') .toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); } function parseJson(value, fallback = []) { try { return JSON.parse(value); } catch { return fallback; } } async function refresh(database, fetchImpl = globalThis.fetch) { if (typeof fetchImpl !== 'function') throw new Error('A fetch implementation is required'); const [channelsResponse, feedsResponse] = await Promise.all([ fetchImpl(`${API_ROOT}/channels.json`), fetchImpl(`${API_ROOT}/feeds.json`) ]); if (!channelsResponse.ok || !feedsResponse.ok) throw new Error('iptv-org API refresh failed'); const channels = await channelsResponse.json(); const feeds = await feedsResponse.json(); const feedLanguages = new Map(); for (const feed of feeds) { const list = feedLanguages.get(feed.channel) || new Set(); for (const language of feed.languages || []) list.add(String(language).toLowerCase()); feedLanguages.set(feed.channel, list); } const write = database.transaction(() => { database.prepare('DELETE FROM iptv_org_channels').run(); const insert = database.prepare(` INSERT INTO iptv_org_channels (id, name, normalized_name, alt_names, country, languages, data, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); const now = Date.now(); for (const channel of channels) { insert.run(channel.id, channel.name, normalize(channel.name), JSON.stringify(channel.alt_names || []), channel.country || null, JSON.stringify([...feedLanguages.get(channel.id) || []]), JSON.stringify(channel), now); } database.prepare(` INSERT INTO iptv_org_cache_state (resource, refreshed_at, item_count) VALUES ('catalog', ?, ?) ON CONFLICT(resource) DO UPDATE SET refreshed_at = excluded.refreshed_at, item_count = excluded.item_count `).run(now, channels.length); }); write(); return { refreshedAt: Date.now(), itemCount: channels.length }; } function findMatch(database, channel) { const names = new Set([channel.displayName, channel.fingerprint, ...(channel.variants || []).map(variant => variant.name)].map(normalize).filter(Boolean)); if (!names.size) return null; const direct = database.prepare(`SELECT * FROM iptv_org_channels WHERE normalized_name IN (${[...names].map(() => '?').join(',')})`).all(...names); const candidates = direct.length ? direct : database.prepare('SELECT * FROM iptv_org_channels').all().filter(row => parseJson(row.alt_names).some(name => names.has(normalize(name))) ); const language = channel.language?.code; const compatible = candidates.find(row => language === 'und' || parseJson(row.languages).includes(language)) || candidates[0]; if (!compatible) return null; return { channelId: compatible.id, name: compatible.name, country: compatible.country, confidence: names.has(compatible.normalized_name) ? 100 : 90 }; } module.exports = { API_ROOT, findMatch, normalize, refresh };