#!/usr/bin/env php
<?php
declare(strict_types=1);
ini_set('memory_limit', '-1');
set_time_limit(0);
#const OWNER_USER = 'www-data';
#const OWNER_GROUP = 'www-data';
$URLS = [
'DE' => [
'http://www.xmltvepg.nl/rytecDE_Basic.xz',
'http://www.xmltvepg.nl/rytecDE_Common.xz',
'http://www.xmltvepg.nl/rytecDE_SportMovies.xz',
],
'AT' => ['http://www.xmltvepg.nl/rytecAT_Basic.xz'],
'CH' => ['http://www.xmltvepg.nl/rytecCH_Basic.xz'],
'IPTV' => ['http://www.xmltvepg.nl/rytecIPTV.xz'],
'Misc' => ['http://www.xmltvepg.nl/rytecMisc.xz'],
'NL' => ['http://www.xmltvepg.nl/rytecNL_Basic.xz'],
'BE' => ['http://www.xmltvepg.nl/rytecBE_VL_Basic.xz'],
'TNT' => ['http://www.xmltvepg.nl/rytecTNT_Basic.xz'],
'FR' => ['http://www.xmltvepg.nl/rytecFR_Mixte.xz'],
'BG' => ['http://www.xmltvepg.nl/rytecBG.xz'],
'CZ' => ['http://www.xmltvepg.nl/rytecCZ_Basic.xz'],
'DK' => ['http://www.xmltvepg.nl/rytecDK_Basic.xz'],
'GR' => ['http://www.xmltvepg.nl/rytecGR_Basic.xz'],
'IT' => ['http://www.xmltvepg.nl/rytecIT.xz'],
'PL' => ['http://www.xmltvepg.nl/azman_basic.xml.xz|rytecPL_Basic.xz'],
'PT' => ['http://www.xmltvepg.nl/rytecPT.xz'],
'HU' => ['http://www.xmltvepg.nl/rytecHU_Basic.xz'],
'NO' => ['http://www.xmltvepg.nl/rytecNO_Basic.xz'],
'RO' => ['http://www.xmltvepg.nl/rytecRO_Basic.xz'],
'HRV' => ['http://www.xmltvepg.nl/rytecHRV_Basic.xz'],
'SRB' => ['http://www.xmltvepg.nl/rytecSRB_Basic.xz'],
'SVN' => ['http://www.xmltvepg.nl/rytecSVN_Basic.xz'],
'SE' => ['http://www.xmltvepg.nl/rytecSE_Basic.xz'],
'FI' => ['http://www.xmltvepg.nl/rytecFI_Basic.xz'],
'UK' => ['http://www.xmltvepg.nl/rytecUK_Basic.xz'],
'ERO' => ['http://www.xmltvepg.nl/rytecERO.xz'],
'NWS' => ['http://www.xmltvepg.nl/rytecNWS.xz'],
];
$EPG_SOURCES = [
'DE' => 'Germany',
'AT' => 'Austria',
'CH' => 'Switzerland',
'IPTV' => 'IPTV',
'Misc' => 'Miscellaneous',
'NL' => 'Netherlands',
'BE' => 'Belgium',
'TNT' => 'TNT',
'FR' => 'France',
'BG' => 'Bulgaria',
'CZ' => 'Czech Republic',
'DK' => 'Denmark',
'GR' => 'Greece',
'IT' => 'Italy',
'PL' => 'Poland',
'PT' => 'Portugal',
'HU' => 'Hungary',
'NO' => 'Norway',
'RO' => 'Romania',
'HRV' => 'Croatia',
'SRB' => 'Serbia',
'SVN' => 'Slovenia',
'SE' => 'Sweden',
'FI' => 'Finland',
'UK' => 'United Kingdom',
'ERO' => 'Erotik',
'NWS' => 'News',
];
$JSON_FILE = __DIR__ . '/epgset.json';
$TEMP_DIR = __DIR__ . '/temp';
$TARGET_DIR = __DIR__ . '/epgs';
function clear_screen(): void {
if (strncasecmp(PHP_OS, 'WIN', 3) === 0) system('cls'); else system('clear');
}
function log_info(string $message): void {
$ts = (new DateTimeImmutable('now', new DateTimeZone('Europe/Berlin')))->format('d.m.Y');
fwrite(STDOUT, "[$ts] $message\n");
}
function run_command(string $cmd): array {
$out=[]; $code=1; exec($cmd.' 2>&1',$out,$code);
return ['output'=>implode("\n",$out),'code'=>$code];
}
function safe_chown(string $path): void {}
#function safe_chown(string $path): void {
# if (function_exists('chown')) @chown($path, OWNER_USER);
# if (function_exists('chgrp')) @chgrp($path, OWNER_GROUP);
# @exec('chown '.escapeshellarg(OWNER_USER.':'.OWNER_GROUP).' '.escapeshellarg($path));
#}
function ensure_dirs(string ...$dirs): void {
foreach ($dirs as $dir) {
if (!is_dir($dir)) {
if (!mkdir($dir, 0755, true)) { log_info("Error: cannot create directory: $dir"); exit(1); }
safe_chown($dir); chmod($dir, 0755);
}
}
}
function load_sets(string $file): array {
if (!file_exists($file)) return ['sets'=>[]];
$data = json_decode((string)file_get_contents($file), true);
if (!is_array($data)) return ['sets'=>[]];
if (!isset($data['sets']) || !is_array($data['sets'])) $data['sets'] = [];
return $data;
}
function save_sets(string $file, array $data): void {
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
safe_chown($file); chmod($file, 0755);
}
function next_id(array $sets): int {
$max=0; foreach ($sets as $s) { $max = max($max, (int)($s['id'] ?? 0)); } return $max+1;
}
function prompt(string $q, ?string $def=null): string {
fwrite(STDOUT, $q.($def!==null?" [$def]":"").": ");
$line = fgets(STDIN); if ($line===false) return $def ?? '';
$line = trim($line); return ($line==='' && $def!==null) ? $def : $line;
}
function choose_countries(array $sources): array {
fwrite(STDOUT, "Select countries (comma-separated by index or codes). Example: 1,3,5 or DE,AT,CH\n\n");
$codes=array_keys($sources); $i=1;
foreach ($sources as $code=>$name) { printf("%2d) %-4s %s\n",$i,$code,$name); $i++; }
fwrite(STDOUT,"\n");
$inp = strtoupper(trim(prompt("Enter selection")));
if ($inp==='') return [];
$parts = array_map('trim', explode(',',$inp)); $picked=[];
foreach ($parts as $p) {
if ($p==='') continue;
if (ctype_digit($p)) { $j=(int)$p; if ($j>=1 && $j<=count($codes)) $picked[]=$codes[$j-1]; }
else { if (isset($sources[$p])) $picked[]=$p; }
}
return array_values(array_unique($picked));
}
function list_sets(array $sets): void {
if (empty($sets)) { fwrite(STDOUT,"No saved sets.\n"); return; }
fwrite(STDOUT,"Saved EPG sets:\n");
foreach ($sets as $s) {
$id=$s['id']; $fn=$s['filename']??''; $cc=implode(',', $s['countries']??[]);
$ts=$s['created_at']??''; $m = !empty($s['magenta']['include']) ? " (MagentaSport: {$s['magenta']['days']}d)" : "";
printf(" %d) %-25s [%s] %s%s\n",$id,$fn,$cc,$ts,$m);
}
}
function uuidv4(): string {
$data = random_bytes(16);
$data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
$data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
function magenta_cookie_file(string $tmp): string {
return rtrim($tmp, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'magenta_cookies.txt';
}
function magenta_csrf_from_cookie(string $cookieFile): ?string {
if (!is_file($cookieFile)) return null;
$lines = @file($cookieFile, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
if (!$lines) return null;
foreach ($lines as $line) {
if (strlen($line)===0 || $line[0]==='#') continue;
$parts = explode("\t", $line);
if (count($parts)>=7) {
$name = $parts[5]; $val = $parts[6];
if ($name==='CSRFSESSION') return trim($val);
}
}
return null;
}
function http_post_json_with_cookies(string $url, string $json, string $cookieFile, array $headers=[]): array {
$h = [];
$h[] = 'Content-Type: application/json';
$h[] = 'Accept: application/json';
foreach ($headers as $k=>$v) { $h[] = $k.': '.$v; }
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $json,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => $h,
CURLOPT_COOKIEFILE => $cookieFile,
CURLOPT_COOKIEJAR => $cookieFile,
CURLOPT_USERAGENT => 'EPGBuilder/1.0 (MagentaSport)',
CURLOPT_HEADER => false,
]);
$body = curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
return ['code'=>$code, 'body'=>$body, 'err'=>$err];
}
function magenta_login(string $tmp): ?string {
ensure_dirs($tmp);
$cookieFile = magenta_cookie_file($tmp);
@unlink($cookieFile);
$headers = [];
$tries = 0;
while ($tries < 120) {
$mac = uuidv4(); $ter = uuidv4();
$auth_data = json_encode([
"areaid"=>"1","cnonce"=>"c4b11948545fb3089720dd8b12c81f8e",
"mac"=>$mac,"preSharedKeyID"=>"NGTV000001","subnetId"=>"4901",
"templatename"=>"NGTV","terminalid"=>$ter,"terminaltype"=>"WEB-MTV",
"terminalvendor"=>"WebTV","timezone"=>"UTC","usergroup"=>"-1",
"userType"=>3,"utcEnable"=>1
], JSON_UNESCAPED_SLASHES);
$res = http_post_json_with_cookies('https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate', $auth_data, $cookieFile);
if ($res['code']!==200 || $res['body']===false) { $tries++; usleep(100000); continue; }
$j = json_decode((string)$res['body'], true);
if (isset($j['retcode']) && $j['retcode']==='-2') { $tries++; usleep(100000); continue; }
break;
}
$csrf = magenta_csrf_from_cookie($cookieFile);
if (!$csrf) { log_info("Magenta: CSRF cookie not found."); return null; }
return $cookieFile;
}
function magenta_fetch_channels(string $cookieFile): array {
$csrf = magenta_csrf_from_cookie($cookieFile);
if (!$csrf) return [];
$payload = '{"properties":[{"name":"logicalChannel","include":"/channellist/logicalChannel/contentId,/channellist/logicalChannel/name"}],"metaDataVer":"Channel/1.1","channelNamespace":"2","filterlist":[{"key":"IsHide","value":"-1"}],"returnSatChannel":0}';
$res = http_post_json_with_cookies(
'https://api.prod.sngtv.magentatv.de/EPG/JSON/AllChannel',
$payload,
$cookieFile,
["X_csrftoken"=>$csrf]
);
if ($res['code'] !== 200 || $res['body'] === false) return [];
$j = json_decode((string)$res['body'], true);
$list = $j['channellist'] ?? [];
$map = [];
foreach ($list as $ch) {
$cid = (string)($ch['contentId'] ?? '');
$name = (string)($ch['name'] ?? '');
if ($cid === '' || $name === '') continue;
if (preg_match('/^Sport\s+\d+\s+-\s+myTeamTV$/', $name)) {
$map[$cid] = $name;
}
}
uasort($map, function($a, $b) {
preg_match('/^Sport\s+(\d+)/', $a, $ma);
preg_match('/^Sport\s+(\d+)/', $b, $mb);
return (int)($ma[1] ?? 0) <=> (int)($mb[1] ?? 0);
});
return $map;
}
function magenta_time_utc_window(int $days, DateTimeZone $tzLocal): array {
$today = new DateTimeImmutable('now', $tzLocal);
$links = [];
for ($day=0; $day<$days; $day++) {
$sd = $today->setTime(6,0,0)->modify("+$day day");
$ed = $today->setTime(5,59,0)->modify("+".($day+1)." day");
$ts = $sd->setTimezone(new DateTimeZone('UTC'))->format('YmdHis');
$te = $ed->setTimezone(new DateTimeZone('UTC'))->format('YmdHis');
$links[] = ['begin'=>$ts, 'end'=>$te];
}
return $links;
}
function magenta_fetch_playbills(string $cookieFile, array $channels, int $days): array {
$csrf = magenta_csrf_from_cookie($cookieFile);
if (!$csrf) return [];
$tzLocal = new DateTimeZone('Europe/Berlin');
$windows = magenta_time_utc_window($days, $tzLocal);
$allowed = array_fill_keys(array_map('strval', array_keys($channels)), true); // only myTeamTV channels
$all = [];
foreach ($windows as $w) {
$p = [
"type"=>2,"isFiltrate"=>0,"orderType"=>4,"isFillProgram"=>1,
"channelNamespace"=>"2","offset"=>0,"count"=>-1,
"properties"=>[["name"=>"playbill","include"=>implode(",",[
"subName","id","name","starttime","endtime","channelid",
"ratingid","genres","introduce","cast","country","pictures",
"producedate","seasonNum","subNum"
])]],
"endtime"=>$w['end'],"begintime"=>$w['begin']
];
$res = http_post_json_with_cookies(
'https://api.prod.sngtv.magentatv.de/EPG/JSON/PlayBillList',
json_encode($p, JSON_UNESCAPED_SLASHES),
$cookieFile,
["X_csrftoken"=>$csrf]
);
if ($res['code'] !== 200 || $res['body'] === false) continue;
$j = json_decode((string)$res['body'], true);
$pl = $j['playbilllist'] ?? [];
foreach ($pl as $prog) {
$cid = (string)($prog['channelid'] ?? '');
if ($cid === '' || !isset($allowed[$cid])) continue;
$stRaw = (string)($prog['starttime'] ?? '');
$enRaw = (string)($prog['endtime'] ?? '');
$st = str_replace(' UTC+00:00','',$stRaw);
$en = str_replace(' UTC+00:00','',$enRaw);
$stTs = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $st, new DateTimeZone('UTC'));
$enTs = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $en, new DateTimeZone('UTC'));
if (!$stTs || !$enTs) continue;
$all[] = [
'c_id' => $cid, // string
'start' => $stTs->format('YmdHis').' +0000',
'end' => $enTs->format('YmdHis').' +0000',
'title' => (string)($prog['name'] ?? ''),
'subtitle'=> $prog['subName'] ?? null,
'desc' => $prog['introduce'] ?? null,
];
}
}
return $all;
}
function magenta_write_xml(string $tmp, array $channels, array $airings): ?string {
if (empty($channels)) { log_info("Magenta: no myTeamTV channels."); return null; }
$file = rtrim($tmp, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'magenta_sport.xml';
$w = new XMLWriter();
if (!$w->openURI($file)) { log_info("Magenta: cannot open output file."); return null; }
$w->startDocument('1.0', 'UTF-8');
$w->setIndent(true);
$w->startElement('tv');
foreach ($channels as $cid => $name) {
$w->startElement('channel');
$w->writeAttribute('id', (string)$cid);
$w->startElement('display-name'); $w->text((string)$name); $w->endElement();
$w->endElement();
}
foreach ($airings as $p) {
$w->startElement('programme');
$w->writeAttribute('start', (string)$p['start']);
$w->writeAttribute('stop', (string)$p['end']);
$w->writeAttribute('channel', (string)$p['c_id']);
$w->startElement('title'); $w->writeAttribute('lang', 'de'); $w->text((string)($p['title'] ?? '')); $w->endElement();
if (!empty($p['subtitle'])) { $w->startElement('sub-title'); $w->text((string)$p['subtitle']); $w->endElement(); }
if (!empty($p['desc'])) { $w->startElement('desc'); $w->text((string)$p['desc']); $w->endElement(); }
$w->endElement();
}
$w->endElement();
$w->endDocument();
$w->flush();
safe_chown($file);
chmod($file, 0644);
log_info("Magenta: XML generated -> $file");
return $file;
}
function magenta_build_xml_into_tmp(string $tmp, int $days=5): ?string {
log_info("Magenta: authenticating ...");
$cookie = magenta_login($tmp);
if (!$cookie) { log_info("Magenta: login failed."); return null; }
$channels = magenta_fetch_channels($cookie);
if (empty($channels)) { log_info("Magenta: channel list empty."); return null; }
log_info("Magenta: fetched ".count($channels)." channels.");
$air = magenta_fetch_playbills($cookie, $channels, $days);
log_info("Magenta: collected ".count($air)." programme entries.");
return magenta_write_xml($tmp, $channels, $air);
}
function download_and_extract(string $url, string $dir): ?string {
if (strpos($url,'|')!==false) { [$url,$renamed] = explode('|',$url,2); } else $renamed=null;
$parts = parse_url($url);
if (!$parts || !isset($parts['scheme']) || !in_array($parts['scheme'], ['http','https'], true)) { log_info("Error: invalid URL $url"); return null; }
$orig = basename($parts['path'] ?? '');
if ($orig==='') { log_info("Error: cannot determine filename for $url"); return null; }
$dl = rtrim($dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$orig;
$tfn = $renamed ? preg_replace('/\.(?:xz|gz|xml\.gz)$/i','.xml',$renamed)
: preg_replace('/\.(?:xz|gz|xml\.gz)$/i','.xml',$orig);
$target = rtrim($dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$tfn;
log_info("Downloading $url ...");
$ch = curl_init($url); $fp = @fopen($dl,'wb');
if (!$fp) { log_info("Error: cannot open for write: $dl"); return null; }
curl_setopt_array($ch,[
CURLOPT_FILE=>$fp, CURLOPT_FOLLOWLOCATION=>true, CURLOPT_FAILONERROR=>true,
CURLOPT_TIMEOUT=>60, CURLOPT_CONNECTTIMEOUT=>10, CURLOPT_USERAGENT=>'EPGBuilder/1.0',
CURLOPT_SSL_VERIFYPEER=>true, CURLOPT_SSL_VERIFYHOST=>2,
]);
$res=curl_exec($ch); $code=(int)curl_getinfo($ch,CURLINFO_HTTP_CODE); $err=curl_error($ch);
curl_close($ch); fclose($fp);
if ($res===false || $code!==200) { log_info("Error: download failed: $url (HTTP $code) curl: $err"); @unlink($dl); return null; }
$ext=strtolower(pathinfo($dl, PATHINFO_EXTENSION));
if ($ext==='xz' || $ext==='gz') {
log_info("Extracting $dl ...");
$ok=false;
if ($ext==='xz') {
$which = run_command('command -v xz');
if ($which['code']!==0) log_info("Error: 'xz' not found for $dl");
else {
$cmd='xz -dc '.escapeshellarg($dl).' > '.escapeshellarg($target);
$r = run_command($cmd);
if (file_exists($target) && filesize($target)>0) $ok=true;
else log_info("Error: xz decompression failed. Output:\n".$r['output']);
}
} else {
$gz=@gzopen($dl,'rb');
if ($gz!==false) {
$out=@fopen($target,'wb');
if ($out!==false) {
while (!gzeof($gz)) { $chunk=gzread($gz,8192); if ($chunk===false) break; fwrite($out,$chunk); }
fclose($out); $ok=true;
} else log_info("Error: cannot open target for write: $target");
gzclose($gz);
} else log_info("Error: cannot open gzip file: $dl");
}
@unlink($dl); if (!$ok) return null;
} else { log_info("Error: unsupported extension for $dl"); @unlink($dl); return null; }
safe_chown($target); chmod($target,0644); log_info("Extracted: $target");
return $target;
}
function build_epg(string $tmp, string $dst, array $xmls, string $out): void {
if (empty($xmls)) { log_info("Error: no XML files to process."); exit(1); }
$final = rtrim($dst, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$out;
$part = $final.'.part';
$channels=[]; $progs=[];
log_info("Merging ".count($xmls)." XML files ...");
foreach ($xmls as $file) {
if (!is_readable($file)) { log_info("Warn: unreadable: $file"); continue; }
$r=new XMLReader(); if (!$r->open($file)) { log_info("Warn: cannot open xml: $file"); continue; }
while ($r->read()) {
if ($r->nodeType===XMLReader::ELEMENT) {
$n=strtolower($r->name);
if ($n==='channel') {
$id=$r->getAttribute('id') ?? ''; $x=$r->readOuterXml();
if ($id==='') $channels[]=$x; elseif (!isset($channels[$id])) $channels[$id]=$x;
} elseif ($n==='programme') { $progs[]=$r->readOuterXml(); }
}
}
$r->close();
}
$w=new XMLWriter(); if (!$w->openURI($part)) { log_info("Error: cannot open temp file: $part"); exit(1); }
$w->startDocument('1.0','UTF-8'); $w->setIndent(true);
$w->startElement('tv'); foreach ($channels as $c) $w->writeRaw($c); foreach ($progs as $p) $w->writeRaw($p); $w->endElement();
$w->endDocument(); $w->flush();
if (!@rename($part,$final)) { @unlink($part); log_info("Error: cannot finalize output to $final"); exit(1); }
safe_chown($final); chmod($final,0755); log_info("EPG saved: $final");
$gz=$final.'.gz'; $in=@fopen($final,'rb');
if ($in) {
$outf=@gzopen($gz,'wb9');
if ($outf) { while (!feof($in)) { $c=fread($in,8192); if ($c===false) break; gzwrite($outf,$c);} fclose($in); gzclose($outf);
if (file_exists($gz)) { safe_chown($gz); chmod($gz,0755); log_info("EPG gzipped: $gz"); }
} else log_info("Warn: cannot open gzip for write: $gz");
} else log_info("Warn: cannot reopen output for gzip: $final");
}
function cleanup_temp(string $dir): void {
log_info("Cleaning temp: $dir");
if (!is_dir($dir)) return;
$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
foreach ($it as $f) { if ($f->isDir()) @rmdir($f->getRealPath()); else @unlink($f->getRealPath()); }
@rmdir($dir);
}
function build_from_codes(array $codes, array $URLS, string $TMP, string $DST, ?string $rename=null, bool $includeMagenta=false, int $magentaDays=7): void {
if (empty($codes)) { log_info("No country codes selected."); exit(1); }
ensure_dirs($TMP,$DST);
$selected=[]; foreach ($codes as $c) { if (isset($URLS[$c])) $selected=array_merge($selected,$URLS[$c]); else log_info("Warn: unknown code $c (skipped)"); }
if (empty($selected) && !$includeMagenta) { log_info("No URLs found for selected codes."); exit(1); }
$xml=[];
foreach ($selected as $u) { $f=download_and_extract($u,$TMP); if ($f) $xml[]=$f; }
if ($includeMagenta) {
$mf = magenta_build_xml_into_tmp($TMP, max(1,$magentaDays));
if ($mf) $xml[] = $mf;
}
foreach (glob(rtrim($TMP,DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'*.xml') as $extra) {
if (!in_array($extra,$xml,true)) $xml[]=$extra;
}
if (empty($xml)) { log_info("Error: nothing to merge."); exit(1); }
$out = $rename ?: implode('_',$codes).'.xml';
build_epg($TMP,$DST,$xml,$out);
cleanup_temp($TMP);
log_info("Done.");
}
$mode = $argv[1] ?? '';
if ($mode === 'build') {
clear_screen();
$data = load_sets($JSON_FILE);
$sets = $data['sets'];
if (empty($sets)) { log_info("No saved sets in epgset.json. Run interactively to create sets first."); exit(1); }
$req=[];
if ($argc>2) {
for ($i=2;$i<$argc;$i++) {
$arg=trim($argv[$i]); if ($arg==='') continue;
foreach (explode(',',$arg) as $p) { $p=trim($p); if ($p!=='' && ctype_digit($p)) $req[]=(int)$p; }
}
$req=array_values(array_unique($req));
if (empty($req)) { log_info("No valid set numbers given. Nothing to build."); exit(1); }
} else { $req = array_map(fn($s)=>(int)$s['id'],$sets); }
$byId=[]; foreach ($sets as $s) $byId[(int)$s['id']]=$s;
foreach ($req as $id) {
if (!isset($byId[$id])) { log_info("Skip unknown set id: $id"); continue; }
$s=$byId[$id];
$codes=array_values(array_unique(array_map('strtoupper',$s['countries'] ?? [])));
$fn=$s['filename'] ?? (implode('_',$codes).'.xml');
$incMag = !empty($s['magenta']['include']);
$magDays = (int)($s['magenta']['days'] ?? 5);
log_info("Building set #$id -> $fn [".implode(',',$codes)."]".($incMag?" + MagentaSport($magDays d)":""));
build_from_codes($codes, $URLS, $TEMP_DIR, $TARGET_DIR, $fn, $incMag, $magDays);
}
exit(0);
}
clear_screen();
log_info("EPG Builder (CLI)");
ensure_dirs($TARGET_DIR);
$data = load_sets($JSON_FILE);
$sets = $data['sets'];
while (true) {
clear_screen();
fwrite(STDOUT, "\nMenu: [C]reate new set | [B]uild existing | [D]elete EPG | [Q]uit\n");
$ch = strtolower(prompt("Choose", "c"));
if ($ch === 'q') {
log_info("Bye."); exit(0);
} elseif ($ch === 'b') {
clear_screen();
if (empty($sets)) { fwrite(STDOUT,"No saved sets yet.\n"); continue; }
list_sets($sets);
$sel = trim(prompt("Enter numbers to build (e.g. 1,3,5) or leave empty to cancel", ""));
if ($sel==='') continue;
$ids=[]; foreach (explode(',',$sel) as $p) { $p=trim($p); if ($p!=='' && ctype_digit($p)) $ids[]=(int)$p; }
$ids=array_values(array_unique($ids));
if (empty($ids)) { fwrite(STDOUT,"No valid numbers.\n"); continue; }
$byId=[]; foreach ($sets as $s) $byId[(int)$s['id']]=$s;
foreach ($ids as $id) {
if (!isset($byId[$id])) { log_info("Skip unknown set id: $id"); continue; }
$s=$byId[$id];
$codes=array_values(array_unique(array_map('strtoupper',$s['countries'] ?? [])));
$fn=$s['filename'] ?? (implode('_',$codes).'.xml');
$incMag = !empty($s['magenta']['include']);
$magDays = (int)($s['magenta']['days'] ?? 5);
log_info("Building set #$id -> $fn [".implode(',',$codes)."]".($incMag?" + MagentaSport($magDays d)":""));
build_from_codes($codes, $URLS, $TEMP_DIR, $TARGET_DIR, $fn, $incMag, $magDays);
}
continue;
} elseif ($ch === 'd') {
clear_screen();
if (empty($sets)) { fwrite(STDOUT,"No saved sets yet.\n"); continue; }
list_sets($sets);
$sel = trim(prompt("Enter numbers to delete (e.g. 1,3,5) or leave empty to cancel", ""));
if ($sel==='') continue;
$ids=[]; foreach (explode(',',$sel) as $p) { $p=trim($p); if ($p!=='' && ctype_digit($p)) $ids[]=(int)$p; }
$ids=array_values(array_unique($ids));
if (empty($ids)) { fwrite(STDOUT,"No valid numbers.\n"); continue; }
$byId=[]; foreach ($sets as $s) $byId[(int)$s['id']]=$s;
foreach ($ids as $id) {
if (!isset($byId[$id])) { log_info("Skip unknown set id: $id"); continue; }
$s=$byId[$id]; $fn=$s['filename'] ?? (implode('_',$s['countries'] ?? []).'.xml');
$file=$TARGET_DIR.DIRECTORY_SEPARATOR.$fn; $gz=$file.'.gz';
if (file_exists($file)) { unlink($file); log_info("Deleted: $file"); }
if (file_exists($gz)) { unlink($gz); log_info("Deleted: $gz"); }
$ans = strtolower(prompt("Remove set #$id from epgset.json? (y/n)","n"));
if ($ans==='y') {
$sets = array_values(array_filter($sets, fn($x)=>(int)$x['id']!==$id));
$data['sets'] = $sets; save_sets($JSON_FILE, $data);
log_info("Removed set #$id from epgset.json");
}
}
continue;
} elseif ($ch === 'c') {
clear_screen();
while (true) {
$codes = choose_countries($EPG_SOURCES);
if (empty($codes)) { fwrite(STDOUT,"No selection. Cancelled.\n"); break; }
$def = implode('_',$codes).'.xml';
$fn = prompt("Output filename", $def); if (trim($fn)==='') $fn=$def;
$incMag = strtolower(prompt("Include MagentaSport? (y/n)", "n"))==='y';
$magDays = 5;
if ($incMag) {
$d = trim(prompt("How many days for MagentaSport?", "7"));
if (ctype_digit($d) && (int)$d>=1 && (int)$d<=14) $magDays=(int)$d;
}
$newId = next_id($sets);
$entry = [
'id' => $newId,
'countries' => array_values(array_unique(array_map('strtoupper',$codes))),
'filename' => $fn,
'created_at' => (new DateTimeImmutable('now', new DateTimeZone('Europe/Berlin')))->format('d.m.Y'),
'magenta' => ['include'=>$incMag,'days'=>$magDays],
];
$sets[]=$entry; $data['sets']=$sets; save_sets($JSON_FILE,$data);
log_info("Saved as set #$newId -> $fn [".implode(',',$entry['countries'])."]".($incMag?" + MagentaSport($magDays d)":""));
$more = strtolower(prompt("Add another set? (y/n)", "n")); if ($more!=='y') break;
}
if (!empty($sets)) {
list_sets($sets);
$tb = trim(prompt("Build which set numbers now? (e.g. 1,3) or leave empty to skip", ""));
if ($tb!=='') {
$ids=[]; foreach (explode(',',$tb) as $p) { $p=trim($p); if ($p!=='' && ctype_digit($p)) $ids[]=(int)$p; }
$ids=array_values(array_unique($ids));
$byId=[]; foreach ($sets as $s) $byId[(int)$s['id']]=$s;
foreach ($ids as $id) {
if (!isset($byId[$id])) { log_info("Skip unknown set id: $id"); continue; }
$s=$byId[$id];
$codes=array_values(array_unique(array_map('strtoupper',$s['countries'] ?? [])));
$fn=$s['filename'] ?? (implode('_',$codes).'.xml');
$incMag = !empty($s['magenta']['include']);
$magDays = (int)($s['magenta']['days'] ?? 5);
log_info("Building set #$id -> $fn [".implode(',',$codes)."]".($incMag?" + MagentaSport($magDays d)":""));
build_from_codes($codes, $URLS, $TEMP_DIR, $TARGET_DIR, $fn, $incMag, $magDays);
}
}
}
continue;
} else {
fwrite(STDOUT, "Invalid choice.\n"); continue;
}
}
?>