grab-epg.ts 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  1. #!/usr/bin/env bun
  2. // Batched EPG grab — keeps memory bounded. The iptv-org grabber holds every
  3. // programme for every channel in RAM until the end (1000+ channels = many GB),
  4. // so we grab ~BATCH channels at a time, EACH IN ITS OWN bun process (memory is
  5. // reclaimed when the process exits), then merge the per-batch guides into one
  6. // XMLTV file. Tunable via env: BATCH, DAYS, MAXCONN, LIMIT, CHANNELS, OUT.
  7. import { $ } from 'bun';
  8. import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
  9. const HERE = import.meta.dir;
  10. const TMP = '/tmp/iptv-org-build';
  11. const EPG = `${TMP}/epg`;
  12. const CHANNELS = process.env.CHANNELS || `${TMP}/custom.channels.xml`;
  13. const OUT = process.env.OUT || `${HERE}/guide.xml`;
  14. const BATCH = Number(process.env.BATCH || 25); // ~3 GB peak per batch (measured)
  15. const DAYS = Number(process.env.DAYS || 2);
  16. const MAXCONN = Number(process.env.MAXCONN || 10);
  17. const LIMIT = Number(process.env.LIMIT || 0); // 0 = all batches (for quick tests)
  18. const channels = [...readFileSync(CHANNELS, 'utf8').matchAll(/<channel\b[\s\S]*?<\/channel>/g)].map((m) => m[0]);
  19. const dir = `${TMP}/batches`;
  20. rmSync(dir, { recursive: true, force: true });
  21. mkdirSync(dir, { recursive: true });
  22. let groups = [];
  23. for (let i = 0; i < channels.length; i += BATCH) groups.push(channels.slice(i, i + BATCH));
  24. if (LIMIT) groups = groups.slice(0, LIMIT);
  25. console.log(`${channels.length} channels -> ${groups.length} batch(es) of ${BATCH} (days=${DAYS}, maxConn=${MAXCONN})`);
  26. const guides = [];
  27. for (let i = 0; i < groups.length; i++) {
  28. const cf = `${dir}/b${i}.channels.xml`;
  29. const gf = `${dir}/g${i}.xml`;
  30. writeFileSync(cf, `<?xml version="1.0" encoding="UTF-8"?>\n<channels>\n${groups[i].join('\n')}\n</channels>\n`);
  31. await $`bun scripts/commands/epg/grab.ts --channels ${cf} --output ${gf} --days ${DAYS} --maxConnections ${MAXCONN}`
  32. .cwd(EPG).quiet().nothrow();
  33. const n = existsSync(gf) ? (readFileSync(gf, 'utf8').match(/<programme\b/g) || []).length : -1;
  34. console.log(` [${i + 1}/${groups.length}] ${n < 0 ? 'FAILED' : `${n} programmes`}`);
  35. if (existsSync(gf)) guides.push(gf);
  36. }
  37. // merge: every channel is in exactly one batch, so no dedup needed
  38. const chans = [], progs = [];
  39. for (const g of guides) {
  40. const t = readFileSync(g, 'utf8');
  41. for (const m of t.matchAll(/<channel\b[\s\S]*?<\/channel>/g)) chans.push(m[0]);
  42. for (const m of t.matchAll(/<programme\b[\s\S]*?<\/programme>/g)) progs.push(m[0]);
  43. }
  44. writeFileSync(OUT, `<?xml version="1.0" encoding="UTF-8"?>\n<tv>\n${chans.join('\n')}\n${progs.join('\n')}\n</tv>\n`);
  45. console.log(`merged -> ${OUT} (${chans.length} channels, ${progs.length} programmes)`);