Add backend language detection, flexible live channel search, iptv-org catalog caching, and protected built-in EPG source registration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
66 lines
3.2 KiB
JavaScript
66 lines
3.2 KiB
JavaScript
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 };
|