05 Mar 2026
16 Ramadan 1447

TEL: 01494 259 194

St Marks Close, High Wycombe,  HP13 6GN

Asset 1
Prayer
Times
Start
Jama'ah
Asset 2
Fajr

05:06

05:21

Dhuhr Jama'ah

12:45

4 Hours 39 Minutes
Asset 5
Sunrise

06:41

Asset 3
Dhuhr

12:17

12:45

Asset 4
Asr

15:11

15:30

Asset 5
Maghrib

17:51

18:01

Asset 6
Isha

19:02

19:30

05 Mar 2026
 
16 Ramadan 1447

Asset 1
Prayer
Times
Start
Jama'ah
Asset 2
Fajr

05:06

05:21

Dhuhr Jama'ah

12:45

4 Hours 39 Minutes
Asset 5
Sunrise

06:41

Asset 3
Dhuhr

12:17

12:45

Asset 4
Asr

15:11

15:30

Asset 5
Maghrib

17:51

18:01

Asset 6
Isha

19:02

19:30

05 Mar 2026
 
16 Ramadan 1447

Asset 1
Prayer
Times
Start
Jama'ah
Asset 2
Fajr

05:06

05:21

Asset 5
Sunrise

06:41

Asset 3
Dhuhr

12:17

12:45

Asset 4
Asr

15:11

15:30

Asset 5
Maghrib

17:51

18:01

Asset 6
Isha

19:02

19:30

Dhuhr Jama’ah

12:45

4 Hours 39 Minutes

modify(‘+1 day’);

$todayEvents = [];
foreach ($events as $e) {
// Consider events that overlap today:
// start < tomorrow AND end > todayStart
$start = $e[‘start’];
$end = $e[‘end’];

if ($start < $tomorrowStart && $end > $todayStart) {
$todayEvents[] = $e;
}
}

// Sort by start time
usort($todayEvents, function ($a, $b) {
return $a[‘start’] <=> $b[‘start’];
});

// Limit
if (count($todayEvents) > $MAX_EVENTS) {
$todayEvents = array_slice($todayEvents, 0, $MAX_EVENTS);
}

// ————————-
// RENDER HTML
// ————————-
$html = render_today_html($todayEvents, $todayStart, $TZ);

// Write cache
@file_put_contents($CACHE_FILE, $html);

header(‘Content-Type: text/html; charset=utf-8’);
echo $html;
exit;

// ============================================================
// FUNCTIONS
// ============================================================

function fetch_ics(string $url): ?string
{
// Use cURL if available (more reliable), else fallback to file_get_contents
if (function_exists(‘curl_init’)) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
CURLOPT_USERAGENT => ‘TV-Calendar-Today/1.0’,
]);
$data = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($data !== false && $code >= 200 && $code < 300) { return (string)$data; } return null; } $context = stream_context_create([ 'http' => [
‘timeout’ => 10,
‘header’ => “User-Agent: TV-Calendar-Today/1.0\r\n”,
],
]);

$data = @file_get_contents($url, false, $context);
return $data === false ? null : (string)$data;
}

/**
* Parse ICS and return array of events:
* [
* [‘title’ => string, ‘start’ => DateTimeImmutable, ‘end’ => DateTimeImmutable, ‘allDay’ => bool]
* ]
*
* Notes:
* – Handles basic VEVENT fields only (SUMMARY, DTSTART, DTEND)
* – Handles folded lines
* – Handles DTSTART/DTEND formats:
* – DATE: 20260127 (all-day)
* – DATE-TIME UTC: 20260127T090000Z
* – DATE-TIME local: 20260127T090000
* – TZID param: DTSTART;TZID=Europe/London:20260127T090000
* – Does NOT fully expand RRULE recurrences (kept simple).
*/
function parse_ics_events(string $ics, DateTimeZone $displayTz): array
{
$lines = unfold_ics_lines($ics);

$events = [];
$inEvent = false;
$cur = [
‘title’ => ”,
‘dtstart_raw’ => null,
‘dtend_raw’ => null,
‘dtstart_params’ => [],
‘dtend_params’ => [],
];

foreach ($lines as $line) {
$line = trim($line);
if ($line === ‘BEGIN:VEVENT’) {
$inEvent = true;
$cur = [
‘title’ => ”,
‘dtstart_raw’ => null,
‘dtend_raw’ => null,
‘dtstart_params’ => [],
‘dtend_params’ => [],
];
continue;
}
if ($line === ‘END:VEVENT’) {
if ($inEvent && $cur[‘dtstart_raw’] !== null) {
$start = parse_ical_datetime($cur[‘dtstart_raw’], $cur[‘dtstart_params’], $displayTz);
$end = $cur[‘dtend_raw’] !== null
? parse_ical_datetime($cur[‘dtend_raw’], $cur[‘dtend_params’], $displayTz)
: $start->modify(‘+1 hour’);

// If DTSTART was DATE (all-day), DTEND is usually next day; keep as-is.
$allDay = is_ical_date_only($cur[‘dtstart_raw’], $cur[‘dtstart_params’]);

$events[] = [
‘title’ => $cur[‘title’] !== ” ? $cur[‘title’] : ‘(No title)’,
‘start’ => $start,
‘end’ => $end,
‘allDay’=> $allDay,
];
}
$inEvent = false;
continue;
}

if (!$inEvent) continue;

// Split “KEY;PARAMS:VALUE” format
[$left, $value] = split_ics_key_value($line);
if ($left === null) continue;

[$key, $params] = parse_ics_key_and_params($left);
$key = strtoupper($key);

if ($key === ‘SUMMARY’) {
$cur[‘title’] = unescape_ics_text($value);
} elseif ($key === ‘DTSTART’) {
$cur[‘dtstart_raw’] = $value;
$cur[‘dtstart_params’] = $params;
} elseif ($key === ‘DTEND’) {
$cur[‘dtend_raw’] = $value;
$cur[‘dtend_params’] = $params;
}
}

return $events;
}

function unfold_ics_lines(string $ics): array
{
// Normalize newlines
$ics = str_replace([“\r\n”, “\r”], “\n”, $ics);
$rawLines = explode(“\n”, $ics);

$lines = [];
foreach ($rawLines as $line) {
if ($line === ”) continue;
// Folded line starts with a space or tab and continues previous line
if (!empty($lines) && (isset($line[0]) && ($line[0] === ‘ ‘ || $line[0] === “\t”))) {
$lines[count($lines) – 1] .= substr($line, 1);
} else {
$lines[] = $line;
}
}
return $lines;
}

function split_ics_key_value(string $line): array
{
$pos = strpos($line, ‘:’);
if ($pos === false) return [null, null];
$left = substr($line, 0, $pos);
$value = substr($line, $pos + 1);
return [$left, $value];
}

function parse_ics_key_and_params(string $left): array
{
$parts = explode(‘;’, $left);
$key = array_shift($parts);
$params = [];

foreach ($parts as $p) {
$eq = strpos($p, ‘=’);
if ($eq === false) continue;
$k = strtoupper(substr($p, 0, $eq));
$v = substr($p, $eq + 1);
$params[$k] = $v;
}
return [$key, $params];
}

function is_ical_date_only(string $value, array $params): bool
{
// Either explicit VALUE=DATE or value length 8 (YYYYMMDD)
if (isset($params[‘VALUE’]) && strtoupper($params[‘VALUE’]) === ‘DATE’) return true;
return (bool)preg_match(‘/^\d{8}$/’, $value);
}

function parse_ical_datetime(string $value, array $params, DateTimeZone $displayTz): DateTimeImmutable
{
// DATE (all-day)
if (is_ical_date_only($value, $params)) {
// All-day starts at midnight in display timezone
$dt = DateTimeImmutable::createFromFormat(‘!Ymd’, $value, $displayTz);
return $dt ?: new DateTimeImmutable(‘now’, $displayTz);
}

// TZID param (local time in that TZ)
if (isset($params[‘TZID’])) {
$tz = new DateTimeZone($params[‘TZID’]);
$dt = DateTimeImmutable::createFromFormat(‘!Ymd\THis’, $value, $tz);
if ($dt === false) {
// Some feeds omit seconds: Ymd\THi
$dt = DateTimeImmutable::createFromFormat(‘!Ymd\THi’, $value, $tz);
}
return ($dt ?: new DateTimeImmutable(‘now’, $displayTz))->setTimezone($displayTz);
}

// UTC Z suffix
if (str_ends_with($value, ‘Z’)) {
$v = substr($value, 0, -1);
$utc = new DateTimeZone(‘UTC’);
$dt = DateTimeImmutable::createFromFormat(‘!Ymd\THis’, $v, $utc);
if ($dt === false) $dt = DateTimeImmutable::createFromFormat(‘!Ymd\THi’, $v, $utc);
return ($dt ?: new DateTimeImmutable(‘now’, $displayTz))->setTimezone($displayTz);
}

// Floating local time (assume display TZ)
$dt = DateTimeImmutable::createFromFormat(‘!Ymd\THis’, $value, $displayTz);
if ($dt === false) $dt = DateTimeImmutable::createFromFormat(‘!Ymd\THi’, $value, $displayTz);

return $dt ?: new DateTimeImmutable(‘now’, $displayTz);
}

function unescape_ics_text(string $text): string
{
// ICS escaping: \n, \, \;, \,
$text = str_replace([‘\\n’, ‘\\N’], “\n”, $text);
$text = str_replace([‘\\,’], ‘,’, $text);
$text = str_replace([‘\\;’], ‘;’, $text);
$text = str_replace([‘\\\\’], ‘\\’, $text);
return $text;
}

function render_today_html(array $events, DateTimeImmutable $todayStart, DateTimeZone $tz): string
{
$dateLabel = $todayStart->format(‘l j F Y’); // British style
$items = ”;

if (count($events) === 0) {
$items = ‘

  • No events today
  • ‘;
    } else {
    foreach ($events as $e) {
    $title = htmlspecialchars($e[‘title’], ENT_QUOTES | ENT_SUBSTITUTE, ‘UTF-8’);

    if ($e[‘allDay’]) {
    $time = ‘All day’;
    } else {
    $time = format_time_range($e[‘start’], $e[‘end’], $tz);
    }

    $items .= ‘

  • ‘ . htmlspecialchars($time) . ‘
    . ‘‘ . $title . ‘
  • ‘;
    }
    }

    // Minimal CSS for TV readability
    return ‘





    Today


    Today

    ‘ . htmlspecialchars($dateLabel, ENT_QUOTES, ‘UTF-8’) . ‘
      ‘ . $items . ‘


    ‘;
    }

    function format_time_range(DateTimeImmutable $start, DateTimeImmutable $end, DateTimeZone $tz): string
    {
    $fmt = new IntlDateFormatter(
    ‘en_GB’,
    IntlDateFormatter::NONE,
    IntlDateFormatter::SHORT,
    $tz->getName(),
    IntlDateFormatter::GREGORIAN,
    ‘HH:mm’
    );

    $s = $fmt->format($start);
    $e = $fmt->format($end);

    // If end equals start (rare), show just one time
    if ($s === $e) return (string)$s;
    return $s . ‘–’ . $e;
    }

    function render_error(string $message): string
    {
    $msg = htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, ‘UTF-8’);
    return ‘Error
    . ‘

    ‘ . $msg . ‘


    . ‘‘;
    }

    Scroll to Top