Automated EPG Download

This commit is contained in:
Gogs
2026-07-10 14:14:41 +02:00
parent 8b68ba2e9b
commit 3ae2d14e95
3 changed files with 375 additions and 60 deletions
+9 -1
View File
@@ -1,5 +1,13 @@
FROM php:8.4-apache
RUN echo "memory_limit=512M" > /usr/local/etc/php/conf.d/tvguide.ini
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
xz-utils \
gzip \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN echo "memory_limit=512M" \
> /usr/local/etc/php/conf.d/tvguide.ini
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
+336 -59
View File
@@ -2,84 +2,361 @@
require_once '/var/www/src/bootstrap.php';
$config = new Config(
'/var/www/config/config.json',
'/var/www/config/channels.json'
);
const EPG_WORK_DIR = '/var/www/epg/work';
const EPG_TARGET = '/var/www/epg/guide.xml';
const EPG_CACHE = '/var/www/cache/guide.json';
const EPG_LOCK_FILE = '/var/www/epg/update.lock';
const EPG_LOG_FILE = '/var/www/logs/epg-update.log';
$epg = $config->get('epg', []);
$urls = $epg['urls'] ?? [];
function logMessage(string $message, bool $error = false): void
{
$line = sprintf(
"[%s] %s\n",
date('Y-m-d H:i:s'),
$message
);
if (!$urls && !empty($epg['url'])) {
$urls = [$epg['url']];
echo $line;
if ($error) {
fwrite(STDERR, $line);
}
$logDirectory = dirname(EPG_LOG_FILE);
if (!is_dir($logDirectory)) {
mkdir($logDirectory, 0775, true);
}
file_put_contents(
EPG_LOG_FILE,
$line,
FILE_APPEND | LOCK_EX
);
}
if (!$urls) {
fwrite(STDERR, "Keine epg.urls in config.json gesetzt.\n");
function downloadFile(string $url, string $target): bool
{
$context = stream_context_create([
'http' => [
'timeout' => 120,
'follow_location' => 1,
'max_redirects' => 5,
'user_agent' => 'TVGuide EPG Updater/1.0',
],
'https' => [
'timeout' => 120,
'follow_location' => 1,
'max_redirects' => 5,
'user_agent' => 'TVGuide EPG Updater/1.0',
],
]);
$input = @fopen($url, 'rb', false, $context);
if ($input === false) {
return false;
}
$output = @fopen($target, 'wb');
if ($output === false) {
fclose($input);
return false;
}
$bytes = stream_copy_to_stream($input, $output);
fclose($input);
fclose($output);
return $bytes !== false && $bytes > 0;
}
function detectCompression(string $url, string $file): string
{
$path = strtolower(
parse_url($url, PHP_URL_PATH) ?? ''
);
if (str_ends_with($path, '.xz')) {
return 'xz';
}
if (
str_ends_with($path, '.gz') ||
str_ends_with($path, '.gzip')
) {
return 'gz';
}
$handle = fopen($file, 'rb');
$header = $handle ? fread($handle, 6) : '';
if ($handle) {
fclose($handle);
}
if (str_starts_with($header, "\xFD\x37\x7A\x58\x5A\x00")) {
return 'xz';
}
if (str_starts_with($header, "\x1F\x8B")) {
return 'gz';
}
return 'xml';
}
function unpackFile(
string $source,
string $target,
string $compression
): bool {
if ($compression === 'xml') {
return copy($source, $target);
}
$command = match ($compression) {
'xz' => sprintf(
'xz -dc %s > %s 2>&1',
escapeshellarg($source),
escapeshellarg($target)
),
'gz' => sprintf(
'gzip -dc %s > %s 2>&1',
escapeshellarg($source),
escapeshellarg($target)
),
default => null,
};
if ($command === null) {
return false;
}
exec($command, $output, $status);
return
$status === 0 &&
file_exists($target) &&
filesize($target) > 0;
}
function isValidXmlTv(string $file): bool
{
if (!file_exists($file) || filesize($file) === 0) {
return false;
}
libxml_use_internal_errors(true);
$xml = simplexml_load_file($file);
libxml_clear_errors();
if (!$xml) {
return false;
}
return $xml->getName() === 'tv';
}
function removeOldWorkFiles(): void
{
foreach (glob(EPG_WORK_DIR . '/*') ?: [] as $file) {
if (is_file($file)) {
@unlink($file);
}
}
}
$lockHandle = fopen(EPG_LOCK_FILE, 'c');
if ($lockHandle === false) {
logMessage('Lock-Datei konnte nicht geöffnet werden.', true);
exit(1);
}
$workDir = '/var/www/epg/work';
$target = '/var/www/epg/guide.xml';
if (!is_dir($workDir)) {
mkdir($workDir, 0775, true);
if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
logMessage(
'Ein EPG-Update läuft bereits. Dieser Lauf wird beendet.'
);
fclose($lockHandle);
exit(0);
}
$xmlFiles = [];
$startedAt = microtime(true);
foreach ($urls as $index => $url) {
$base = $workDir . '/source-' . $index;
$xzFile = $base . '.xz';
$xmlFile = $base . '.xml';
try {
logMessage('EPG-Update gestartet.');
echo "Lade EPG: {$url}\n";
$config = new Config(
'/var/www/config/config.json',
'/var/www/config/channels.json'
);
$data = file_get_contents($url);
$epg = $config->get('epg', []);
$urls = $epg['urls'] ?? [];
if ($data === false || trim($data) === '') {
fwrite(STDERR, "Download fehlgeschlagen oder leer: {$url}\n");
continue;
if (!$urls && !empty($epg['url'])) {
$urls = [$epg['url']];
}
file_put_contents($xzFile, $data);
echo "Entpacke XZ...\n";
$command = 'xz -dc ' . escapeshellarg($xzFile) . ' > ' . escapeshellarg($xmlFile);
exec($command, $output, $code);
if ($code !== 0 || !file_exists($xmlFile) || filesize($xmlFile) === 0) {
fwrite(STDERR, "XZ konnte nicht entpackt werden: {$url}\n");
continue;
if (!$urls) {
throw new RuntimeException(
'Keine epg.urls in config.json gesetzt.'
);
}
if (!@simplexml_load_file($xmlFile)) {
fwrite(STDERR, "XML ungültig: {$url}\n");
continue;
if (!is_dir(EPG_WORK_DIR)) {
mkdir(EPG_WORK_DIR, 0775, true);
}
$xmlFiles[] = $xmlFile;
}
removeOldWorkFiles();
$xmlFiles = [];
foreach (array_values($urls) as $index => $url) {
$downloadFile =
EPG_WORK_DIR . "/source-{$index}.download";
$xmlFile =
EPG_WORK_DIR . "/source-{$index}.xml";
logMessage("Lade Quelle " . ($index + 1) . ": {$url}");
if (!downloadFile($url, $downloadFile)) {
logMessage(
"Download fehlgeschlagen oder leer: {$url}",
true
);
continue;
}
$compression = detectCompression(
$url,
$downloadFile
);
logMessage(
sprintf(
'Download abgeschlossen: %.2f MB, Format: %s',
filesize($downloadFile) / 1024 / 1024,
strtoupper($compression)
)
);
if (!unpackFile(
$downloadFile,
$xmlFile,
$compression
)) {
logMessage(
"Datei konnte nicht entpackt werden: {$url}",
true
);
continue;
}
if (!isValidXmlTv($xmlFile)) {
logMessage(
"XMLTV-Datei ist ungültig: {$url}",
true
);
continue;
}
$xmlFiles[] = $xmlFile;
}
if (!$xmlFiles) {
throw new RuntimeException(
'Keine gültigen XMLTV-Dateien geladen.'
);
}
/*
* Zuerst in eine temporäre Datei mergen.
* Die bestehende guide.xml bleibt bei einem Fehler unangetastet.
*/
$temporaryTarget =
EPG_WORK_DIR . '/guide-' . getmypid() . '.xml';
logMessage(
count($xmlFiles) . ' gültige Quelle(n), Merge gestartet.'
);
$merge = new XmlTvMergeService();
$merge->merge($xmlFiles, $temporaryTarget);
if (!isValidXmlTv($temporaryTarget)) {
throw new RuntimeException(
'Die zusammengeführte XMLTV-Datei ist ungültig.'
);
}
/*
* Atomarer Austausch innerhalb desselben Dateisystems.
*/
if (!rename($temporaryTarget, EPG_TARGET)) {
throw new RuntimeException(
'guide.xml konnte nicht atomar ersetzt werden.'
);
}
logMessage(
sprintf(
'guide.xml aktualisiert: %.2f MB',
filesize(EPG_TARGET) / 1024 / 1024
)
);
$cacheService = new XmlTvCacheService(
EPG_TARGET,
$config->get('timezone', 'Europe/Vienna'),
EPG_CACHE
);
$cacheData = $cacheService->rebuildCache();
$channelCount =
count($cacheData['channels'] ?? []);
$programmeCount = 0;
foreach ($cacheData['channels'] ?? [] as $channel) {
$programmeCount +=
count($channel['programmes'] ?? []);
}
$duration = microtime(true) - $startedAt;
logMessage(
sprintf(
'EPG-Update erfolgreich: %d Sender, %d Sendungen, %.1f Sekunden.',
$channelCount,
$programmeCount,
$duration
)
);
exit(0);
} catch (Throwable $exception) {
$duration = microtime(true) - $startedAt;
logMessage(
sprintf(
'EPG-Update fehlgeschlagen nach %.1f Sekunden: %s',
$duration,
$exception->getMessage()
),
true
);
if (!$xmlFiles) {
fwrite(STDERR, "Keine gültigen XMLTV-Dateien geladen.\n");
exit(1);
} finally {
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
}
echo "Merge XMLTV...\n";
$merge = new XmlTvMergeService();
$merge->merge($xmlFiles, $target);
echo "guide.xml aktualisiert.\n";
$cache = new XmlTvCacheService(
$target,
$config->get('timezone', 'Europe/Vienna'),
'/var/www/cache/guide.json'
);
$cache->rebuildCache();
echo "guide.json und guide.version aktualisiert.\n";
+30
View File
@@ -16,3 +16,33 @@ services:
- ./epg:/var/www/epg
- ./logos:/var/www/html/logos
- ./posters:/var/www/html/posters
- ./logs:/var/www/logs
epg-scheduler:
build: .
container_name: tvguide-epg-scheduler
restart: unless-stopped
environment:
EPG_UPDATE_INTERVAL: "21600"
volumes:
- ./app/src:/var/www/src
- ./app/config:/var/www/config
- ./app/cache:/var/www/cache
- ./app/bin:/var/www/bin
- ./vendor:/var/www/vendor
- ./epg:/var/www/epg
- ./logs:/var/www/logs
command:
- /bin/sh
- -c
- |
echo "TVGuide EPG-Scheduler gestartet."
echo "Intervall: $${EPG_UPDATE_INTERVAL:-21600} Sekunden"
while true; do
php /var/www/bin/update-epg.php
sleep "$${EPG_UPDATE_INTERVAL:-21600}"
done