π
debug
xhtml5
source files
validators
server variables

xhtml5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"><head><title>Radio</title><link rel="stylesheet" href="/style/" type="text/css"/><link rel="icon" href="/favicon.png" type="image/png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><style>.bitrate{vertical-align:super;font-size:smaller}</style></head><body><span style="position:fixed;bottom:0;right:0"><a href="?debug">π</a></span><div class="header"><h1 title="move your mouse :)">Radio</h1><br/><fieldset><legend>info</legend><div class="info">To play these radio stations you can use media player <a href="https://mpv.io/" target="_blank">mpv</a>.</div></fieldset><br/><fieldset><legend>link type</legend><table><tr class="selected"><td><a href="?debug&amp;type=playlist">playlist</a></td></tr><tr><td><a href="?debug&amp;type=direct">direct link</a></td></tr></table></fieldset></div><br/><div class="content"><fieldset><legend>music</legend><table><thead><tr><th><span title="just click-n-play">stream</span></th><th>program</th><th><span title="ooh, maps!">location</span></th><th><span title="only the best for you of course :)">quality</span></th><th>website</th></tr></thead><tbody><tr><td><a href="/m3u8.php?name=ORF%3A+FM4&amp;url=https%3A%2F%2Forf-live.ors-shoutcast.at%2Ffm4-q2a" title="originally US army base radio station for the soldiers that were stationed in Austria after the war">ORF: FM4</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Vienna%2C+Austria" target="_blank">Vienna, Austria</a></td><td>MP3<span class="bitrate">192</span></td><td><a href="http://fm4.orf.at/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=313.FM&amp;url=http%3A%2F%2Ficecast.ofdoom.com%3A8000%2Fburst.mp3" title="Detroit&#039;s Electronic Music Station">313.FM</a></td><td>House</td><td><a href="https://maps.google.com/maps/?q=Detroit%2C+USA" target="_blank">Detroit, USA</a></td><td>MP3<span class="bitrate">128</span></td><td><a href="http://313.fm/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Studio+Brussel&amp;url=http%3A%2F%2Ficecast.vrtcdn.be%2Fstubru.aac" title="alternative music">Studio Brussel</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Brussels%2C+Belgium" target="_blank">Brussels, Belgium</a></td><td>AAC-LC<span class="bitrate">128</span></td><td><a href="https://stubru.be/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Studio+Brussel+%E2%80%94+Bruut&amp;url=http%3A%2F%2Ficecast.vrtcdn.be%2Fstubru_bruut.aac" title="">Studio Brussel — Bruut</a></td><td>Rock</td><td><a href="https://maps.google.com/maps/?q=Brussels%2C+Belgium" target="_blank">Brussels, Belgium</a></td><td>AAC-LC<span class="bitrate">128</span></td><td><a href="https://stubru.be/bruut" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Studio+Brussel+%E2%80%94+Hooray&amp;url=http%3A%2F%2Ficecast.vrtcdn.be%2Fstubru_hiphophooray.aac" title="">Studio Brussel — Hooray</a></td><td>Hiphop</td><td><a href="https://maps.google.com/maps/?q=Brussels%2C+Belgium" target="_blank">Brussels, Belgium</a></td><td>AAC-LC<span class="bitrate">128</span></td><td><a href="http://stubru.be/hooray" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Studio+Brussel+%E2%80%94+De+Tijdloze&amp;url=http%3A%2F%2Ficecast.vrtcdn.be%2Fstubru_tijdloze.aac" title="">Studio Brussel — De Tijdloze</a></td><td>Classic Rock</td><td><a href="https://maps.google.com/maps/?q=Brussels%2C+Belgium" target="_blank">Brussels, Belgium</a></td><td>AAC-LC<span class="bitrate">128</span></td><td><a href="http://stubru.be/detijdloze" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Studio+Brussel+%E2%80%94+UNTZ&amp;url=http%3A%2F%2Ficecast.vrtcdn.be%2Fstubru_untz.aac" title="">Studio Brussel — UNTZ</a></td><td>Electronic</td><td><a href="https://maps.google.com/maps/?q=Brussels%2C+Belgium" target="_blank">Brussels, Belgium</a></td><td>AAC-LC<span class="bitrate">128</span></td><td><a href="https://stubru.be/untz" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=YleX&amp;url=https%3A%2F%2Fyleuni-f.akamaihd.net%2Fi%2Fyleliveradiohd_2%40113879%2Findex_256_a-p.m3u8" title="">YleX</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Helsinki%2C+Finland" target="_blank">Helsinki, Finland</a></td><td>AAC-LC<span class="bitrate">256</span></td><td><a href="https://areena.yle.fi/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=DR+P3&amp;url=http%3A%2F%2Fdrradio3-lh.akamaihd.net%2Fi%2Fp3_9%40143506%2Findex_256_a-b.m3u8" title="">DR P3</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Copehagen%2C+Denmark" target="_blank">Copehagen, Denmark</a></td><td>AAC-LC<span class="bitrate">256</span></td><td><a href="https://www.dr.dk/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Couleur3&amp;url=http%3A%2F%2Flsaplus.swisstxt.ch%2Faudio%2Fcouleur3_96.stream%2Fplaylist.m3u8" title="">Couleur3</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Geneva%2C+Switserland" target="_blank">Geneva, Switserland</a></td><td>HE-AAC<span class="bitrate">96</span></td><td><a href="http://www.rts.ch/couleur3/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=SRF3&amp;url=http%3A%2F%2Fstream.srg-ssr.ch%2Fm%2Fdrs3%2Faacp_96" title="youth oriented music from “Schweizer Radio und Fernsehen”">SRF3</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Z%C3%BCrich%2C+Switserland" target="_blank">Zürich, Switserland</a></td><td>HE-AAC<span class="bitrate">96</span></td><td><a href="http://www.srf.ch/radio-srf-3" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=NRK+P3&amp;url=http%3A%2F%2Flyd.nrk.no%3A80%2Fnrk_radio_p3_aac_h" title="youth oriented music channel from “Norsk rikskringkasting”">NRK P3</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Trondheim%2C+Norway" target="_blank">Trondheim, Norway</a></td><td>AAC-LC<span class="bitrate">128</span></td><td><a href="http://p3.no/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Radio+NABA&amp;url=https%3A%2F%2F5a44e5b800a41.streamlock.net%2Fshoutcast%2Fmp4%3Anaba.stream%2Fplaylist.m3u8" title="">Radio NABA</a></td><td>Alt</td><td><a href="https://maps.google.com/maps/?q=Riga%2C+Latvia" target="_blank">Riga, Latvia</a></td><td>HE-AAC<span class="bitrate">112</span></td><td><a href="http://www.naba.lv/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Triple+J&amp;url=https%3A%2F%2Fabcradiolivehls-lh.akamaihd.net%2Fi%2Ftriplejnsw_1%40327300%2Fmaster.m3u8" title="Thanks Andrew :)">Triple J</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Sydney%2C+Australia" target="_blank">Sydney, Australia</a></td><td>AAC-LC<span class="bitrate">96</span></td><td><a href="http://www.abc.net.au/triplej/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Fip&amp;url=https%3A%2F%2Fstream.radiofrance.fr%2Ffip%2Ffip_hifi.m3u8" title="">Fip</a></td><td>Alternative</td><td><a href="https://maps.google.com/maps/?q=Paris%2C+France" target="_blank">Paris, France</a></td><td>MP3<span class="bitrate">192</span></td><td><a href="https://www.radiofrance.fr/fip" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Fip+Rock&amp;url=https%3A%2F%2Fstream.radiofrance.fr%2Ffiprock%2Ffiprock_hifi.m3u" title="">Fip Rock</a></td><td>Alternative/Rock</td><td><a href="https://maps.google.com/maps/?q=Paris%2C+France" target="_blank">Paris, France</a></td><td>MP3<span class="bitrate">192</span></td><td><a href="https://www.radiofrance.fr/fip" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Mouv%27&amp;url=https%3A%2F%2Fstream.radiofrance.fr%2Fmouv%2Fmouv_hifi.m3u8" title="youth oriented music channel from Radio France">Mouv&#039;</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Paris%2C+France" target="_blank">Paris, France</a></td><td>MP3<span class="bitrate">192</span></td><td><a href="http://www.mouv.fr/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Mouv%27+Xtra&amp;url=http%3A%2F%2Fdirect.mouv.fr%2Flive%2Fmouvxtra-midfi.mp3" title="hiphop sister channel of Mouv&#039;">Mouv&#039; Xtra</a></td><td>Hiphop</td><td><a href="https://maps.google.com/maps/?q=Paris%2C+France" target="_blank">Paris, France</a></td><td>MP3<span class="bitrate">128</span></td><td><a href="http://www.mouv.fr/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=BBC+Radio+1Xtra&amp;url=http%3A%2F%2Fa.files.bbci.co.uk%2Fmedia%2Flive%2Fmanifesto%2Faudio%2Fsimulcast%2Fhls%2Fnonuk%2Fsbr_low%2Fllnw%2Fbbc_1xtra.m3u8" title="modern music from the BBC">BBC Radio 1Xtra</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=London%2C+UK" target="_blank">London, UK</a></td><td>HE-AAC<span class="bitrate">96</span></td><td><a href="http://www.bbc.co.uk/1xtra" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Sveriges+Radio+P3&amp;url=http%3A%2F%2Fhttp-live.sr.se%2Fp3-aac-96" title="youth oriented music from the people who brought you IKEA">Sveriges Radio P3</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Stockholm%2C+Sweden" target="_blank">Stockholm, Sweden</a></td><td>HE-AAC<span class="bitrate">96</span></td><td><a href="http://sverigesradio.se/p3/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=1LIVE&amp;url=https%3A%2F%2F1liveuni-lh.akamaihd.net%2Fi%2F1LIVE_HDS%40179577%2Findex_1_a-b.m3u8" title="popular music from WDR">1LIVE</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Cologne%2C+Germany" target="_blank">Cologne, Germany</a></td><td>AAC-LC<span class="bitrate">96</span></td><td><a href="http://www.einslive.de/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=3FM&amp;url=http%3A%2F%2Ficecast.omroep.nl%2F3fm-bb-aac" title="alternative music">3FM</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Hilversum%2C+Netherlands" target="_blank">Hilversum, Netherlands</a></td><td>HE-AAC<span class="bitrate">64</span></td><td><a href="http://www.3fm.nl/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Radio+Activa&amp;url=https%3A%2F%2F27573.live.streamtheworld.com%2FRADIO_ACTIVAAAC.aac" title="Planeta Rock">Radio Activa</a></td><td>Rock</td><td><a href="https://maps.google.com/maps/?q=Bogota%2C+Colombia" target="_blank">Bogota, Colombia</a></td><td>HE-AAC<span class="bitrate">64</span></td><td><a href="http://www.radioacktiva.com/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=%D0%A1%D0%B5%D1%80%D0%B5%D0%B1%D1%80%D1%8F%D0%BD%D1%8B%D0%B9+%D0%94%D0%BE%D0%B6%D0%B4%D1%8C&amp;url=http%3A%2F%2F213.59.4.27%3A8000%2Fsilver64.aac" title="Silver Rain Radio">Серебряный Дождь</a></td><td>Alt</td><td><a href="https://maps.google.com/maps/?q=Moscow%2C+Russia" target="_blank">Moscow, Russia</a></td><td>HE-AAC<span class="bitrate">64</span></td><td><a href="http://www.silver.ru/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=5FM&amp;url=http%3A%2F%2F25483.live.streamtheworld.com%2F5FMAAC.aac" title="from Thosha">5FM</a></td><td>Pop</td><td><a href="https://maps.google.com/maps/?q=Johannesburg%2C+South+Africa" target="_blank">Johannesburg, South Africa</a></td><td>HE-AAC<span class="bitrate">64</span></td><td><a href="http://www.5fm.co.za/" target="_blank">website</a></td></tr></tbody></table></fieldset><br/><fieldset><legend>talk</legend><table><thead><tr><th><span title="just click-n-play">stream</span></th><th>program</th><th><span title="ooh, maps!">location</span></th><th><span title="only the best for you of course :)">quality</span></th><th>website</th></tr></thead><tbody><tr><td><a href="/m3u8.php?name=BBC+Radio+4&amp;url=http%3A%2F%2Fa.files.bbci.co.uk%2Fmedia%2Flive%2Fmanifesto%2Faudio%2Fsimulcast%2Fhls%2Fnonuk%2Fsbr_low%2Fllnw%2Fbbc_radio_fourfm.m3u8">BBC Radio 4</a></td><td>Talk, drama</td><td><a href="https://maps.google.com/maps/?q=London%2C+UK" target="_blank">London, UK</a></td><td>HE-AAC<span class="bitrate">96</span></td><td><a href="http://www.bbc.co.uk/radio4" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Deutschlandfunk&amp;url=https%3A%2F%2Fst03.sslstream.dlf.de%2Fdlf%2F03%2Fhigh%2Fopus%2Fstream.opus" title="educational channel with youth oriented music">Deutschlandfunk</a></td><td>Talk</td><td><a href="https://maps.google.com/maps/?q=Cologne%2C+Germany" target="_blank">Cologne, Germany</a></td><td>Opus<span class="bitrate">64</span></td><td><a href="http://dradiowissen.de/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=France+Inter&amp;url=http%3A%2F%2Fdirect.franceinter.fr%2Flive%2Ffranceinter-midfi.mp3" title="It is a “generalist” station, aiming to provide a wide national audience with a full service of news and spoken-word programming">France Inter</a></td><td>Talk</td><td><a href="https://maps.google.com/maps/?q=Paris%2C+France" target="_blank">Paris, France</a></td><td>MP3<span class="bitrate">128</span></td><td><a href="http://www.franceinter.fr/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=France+Info&amp;url=http%3A%2F%2Fdirect.franceinfo.fr%2Flive%2Ffranceinfo-midfi.mp3" title="News">France Info</a></td><td>News</td><td><a href="https://maps.google.com/maps/?q=Paris%2C+France" target="_blank">Paris, France</a></td><td>MP3<span class="bitrate">128</span></td><td><a href="http://www.franceinfo.fr/" target="_blank">website</a></td></tr><tr><td><a href="/m3u8.php?name=Cadena+SER&amp;url=https%3A%2F%2F20073.live.streamtheworld.com%2FCADENASERAAC.aac%3Fcsegid%3D12000" title="Escucha con nosotros la vida">Cadena SER</a></td><td>Talk, News</td><td><a href="https://maps.google.com/maps/?q=Madrid%2C+Spain" target="_blank">Madrid, Spain</a></td><td>HE-AACv2<span class="bitrate">48</span></td><td><a href="http://www.cadenaser.com/" target="_blank">website</a></td></tr></tbody></table></fieldset><br/><fieldset><legend>chinese</legend>There is a separate page for <a href="/chinese/radio/">Chinese radio</a></fieldset></div></body></html>

source files
/srv/http/radio/index.php
/srv/http/basedoc.php
/srv/http/libs/htmldoc.php
/srv/http/libs/xmldoc.php
/srv/http/libs/doc.php
/srv/http/libs/text.php
/srv/http/libs/xml.php
/srv/http/libs/html_utils.php
/srv/http/libs/css.php
/srv/http/libs/style.php
/srv/http/libs/http.php
/srv/http/debug.php
/srv/http/radio/radio_data.php
/srv/http/debugdoc.php
/srv/http/highlightdoc.php
/srv/http/highlight.php

/srv/http/radio/index.php, 2022-01-20 11:56:02 CET
<?php
require_once('basedoc.php');
require_once('libs/style.php');
require_once('libs/css.php');

function getvar($name) {
    return filter_input(INPUT_GET, $name, FILTER_UNSAFE_RAW);
}

function makeStreamLink($station) {
    $url = $station['link'];
    $name = $station['title'];
    //$tag = makeLink('/m3u8.php', $name, array('name' => $name, 'url' => $url, 'inline' => true));
    $link = makeLink('/m3u8.php', $name, array('name' => $name, 'url' => $url));
    if (array_key_exists('info', $station)) {
        $link->set('title', $station['info']);
    }
    return $link;
}

function makeLocationTag($location) {
    return makeExternalLink(
        'https://maps.google.com/maps/',
        $location,
        array(
            'q' => $location,
            //'t' => 'm', //map type: m(map), k(satelite), p(terrain), h(hybrid), e(google earth)
            //'z' => 10, //zoom level 0-20, doesn't work..
        )
    );
}

$css = makeCss()
    ->add('.bitrate', makeStyle()
        ->set('vertical-align', 'super')
        ->set('font-size', 'smaller')
    );

$linkTypes = array(
    'playlist' => array(
        'desc' => 'playlist',
        'func' => function($station) { return makeStreamLink($station); },
    ),
    'direct' => array(
        'desc' => 'direct link',
        'func' => function($station) { return makeLink($station['link'])->add($station['title']); },
    ),
);

$linkTypeIndex = getvar('type');
if (!array_key_exists($linkTypeIndex, $linkTypes)) {
    $linkTypeIndex = array_keys($linkTypes)[0];
}

$linkFn = $linkTypes[$linkTypeIndex]['func'];

$transforms = array(
    array(
        'title' => makeSpan('stream')->set('title', 'just click-n-play'),
        'transform' => function($station) use ($linkFn) { return $linkFn($station); },
    ),
    array(
        'title' => 'program',
        'transform' => function($station) { return $station['extra']; },
    ),
    array(
        'title' => makeSpan('location')->set('title', 'ooh, maps!'),
        'transform' => function($station) { return makeLocationTag($station['location']); },
    ),
    array(
        'title' => makeSpan('quality')->set('title', 'only the best for you of course :)'),
        'transform' => function($station) { return array($station['codec'], makeSpan($station['bitrate'])->set('class', 'bitrate')); },
    ),
    array(
        'title' => 'website',
        'transform' => function($station) { $site = $station['website'] ; return empty($site) ? null : makeExternalLink($site, 'website'); },
    ),
);

$title = 'Radio';

$doc = makeBasedoc($title);


// Create link type selector
$linkTypeRows = array();
$params = $_GET;
foreach ($linkTypes as $index => $item) {
    $params['type'] = $index;
    $linkTypeRows[$index] = makeTableRow(array(makeLink('', $item['desc'], $params)));
}
$linkTypeRows[$linkTypeIndex]->set('class', 'selected');


$headings = array(
    makeTag('h1', $title)->set('title', 'move your mouse :)'),
);


$mpv = makeExternalLink('https://mpv.io/', 'mpv');
$smiley = makeTag('img')
    ->set('src', 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Emoji_u1f603.svg')
    ->add(makeStyleAttribute(makeStyle()->set('height', '1.5em')->set('vertical-align', 'bottom')));
$headings[] = makeFieldset('info', makeDiv('info', array(
    'To play these radio stations you can use media player ', $mpv, '.',
//', but use whatever works :)',// $smiley,
)));

$headings[] = makeFieldset('link type', makeTag('table', $linkTypeRows));

$blocks = array();

$radioData = include('radio_data.php');

foreach ($radioData as $type => $stations) {
    $blocks[] = makeFieldset($type, makeComplexTable($stations, $transforms));
}

$blocks[] = makeFieldset('chinese', array('There is a separate page for ', makeLink('/chinese/radio/', 'Chinese radio')));

$doc->body()->add(makeLines(array(
    makeDiv('header', makeLines($headings)),
    makeDiv('content', makeLines($blocks)),
)));

$doc->head()->add(makeCssTag($css));

$doc->render();

/srv/http/basedoc.php, 2018-10-19 11:53:24 CEST
<?php
require_once('libs/htmldoc.php');
require_once('libs/html_utils.php');
require_once('libs/style.php');
require_once('libs/http.php');
require_once('debug.php');

class basedoc extends htmldoc {
    function __construct($title, $style = '/style/', $favicon = '/favicon.png') {
        parent::__construct($title);
        $this
            ->setLanguage('en')
            //->add(makeStyleAttribute(makeStyle()->set('background-color', '#000')))
            ;
        $this->head()
            //->add(makeCSSLink('/normalize.css'))
            ->add(makeCSSLink($style))
            ->add(makePngIconLink($favicon))
            ->add(makeMeta('viewport', 'width=device-width,initial-scale=1'))
            //->add(makeHttpEquiv('Cache-control', 'public'))
            //->add(makeHttpEquiv('Cache-control', 'max-age=120'))
            ;
        $bottom_right = makeStyle()->set('position', 'fixed')->set('bottom', '0')->set('right', '0');
        $this->body()->add(makeTag('span', makeLink('?debug', 'π'))->set('style', $bottom_right));

    }

    public function render() {
        makeHttp()
            ->addHeader('Cache-Control', array('public, max-age=120'))
            ->render(makeDebuggable($this, 'xhtml5', 'xml'));
    }
}

function makeBasedoc($title, $style = '/style/', $favicon = '/favicon.png') {
    return new basedoc($title, $style, $favicon);
}

/srv/http/libs/htmldoc.php, 2016-10-16 12:02:05 CEST
<?php
include_once('libs/xmldoc.php');
include_once('libs/html_utils.php');

class htmldoc extends xmldoc {
    private $_head;
    private $_body;

    public function __construct($title) {
        parent::__construct('html', 'application/xhtml+xml');
        $this
            ->setDoctype('html')
            ->set('xmlns', 'http://www.w3.org/1999/xhtml')
            ->add($this->_head = makeTag('head'))
            ->add($this->_body = makeTag('body'))
            ;

        $this->_head
            //->add(makeTag('meta')->set('charset', 'UTF-8'))
            //->add(makeMeta('generator', 'MarcoenMedia'))
            ->add(makeTag('title', $title))
            ;
    }

    public function setLanguage($language) {
        parent::setLanguage($language);
        $this->root()->set('lang', $language);
        return $this;
    }

    public function head() {
        return $this->_head;
    }

    public function body() {
        return $this->_body;
    }
}

function makeHtmldoc($title) {
    return new htmldoc($title);
}


/srv/http/libs/xmldoc.php, 2018-08-02 11:29:36 CEST
<?php
include_once('libs/doc.php');
include_once('libs/xml.php');
include_once('libs/text.php');

class xmldoc extends doc {
    private $_root;
    private $_doctype;

    public function __construct($root, $mimetype = 'application/xml') {
        parent::__construct($mimetype);
        $this->_root = makeTag($root);
    }

    public function setDoctype($doctype) {
        $this->_doctype = $doctype;
        return $this;
    }

    public function setLanguage($language) {
        parent::setLanguage($language);
        $this->_root->set('xml:lang', $language);
        return $this;
    }

    public function root() {
        return $this->_root;
    }

    public function text() {
        $text = array();
        $text[] = '<?xml version="1.0" encoding="UTF-8"?>';
        if ($this->_doctype) {
            $text[] = '<!DOCTYPE ' . $this->_doctype . '>';
        }
        $text[] = $this->_root->text();
        return implode("\n", $text) . "\n";
    }

    public function set($key, $value) {
        $this->_root->set($key, $value);
        return $this;
    }

    public function add($item) {
        $this->_root->add($item);
        return $this;
    }
}

function makeXmldoc($root) {
    return new xmldoc($root);
}

/srv/http/libs/doc.php, 2016-10-17 21:13:58 CEST
<?php
require_once('libs/text.php');

abstract class doc implements toText {
    private $_type;
    private $_encoding;
    private $_language;
    private $_name;

    public function __construct($type /*= 'application/octet-stream'*/, $encoding = 'UTF-8') {
        $this->_type = $type;
        $this->_encoding = $encoding;
    }

    public function setLanguage($language) {
        $this->_language = $language;
        return $this;
    }

    public function setName($name) {
        $this->_name = $name;
        return $this;
    }

    public function type() {
        return $this->_type;
    }

    public function encoding() {
        return $this->_encoding;
    }

    public function language() {
        return $this->_language;
    }

    public function name() {
        return $this->_name;
    }
}


/srv/http/libs/text.php, 2016-01-03 22:34:55 CET
<?php
interface toText {
    public function text();
}

class text implements toText {
    private $_text;

    function __construct($text) {
        $this->_text = $text;
    }

    public function text() {
        return $this->_text;
    }
}

function makeText($value) {
    return new text($value);
}


/srv/http/libs/xml.php, 2021-03-12 19:09:16 CET
<?php
require_once('libs/text.php');

interface container {
    public function isempty();
}

abstract class xml implements toText {
}

class nodelist extends xml implements container {
    private $_children = array();

    public function text() {
        $text = '';
        foreach ($this->_children as $node) {
            if (!is_a($node, 'toText')) continue;
            $text .= is_a($node, 'xml') ? $node->text() : htmlspecialchars($node->text());
        }
        return $text;
    }

    public function add($node) {
        switch (gettype($node)) {
        case 'object':
            if (is_a($node, 'xml') || is_a($node, 'toText')) {
                $this->_children[] = $node;
                break;
            }
            // throw error?
            break;
        case 'array':
            foreach ($node as $subnode) {
                $this->add($subnode);
            }
            break;
        case 'string':
        case 'double':
        case 'integer':
            $this->_children[] = makeText($node);
            break;
        default:
            // throw error?
        }
        return $this;
    }

    public function isempty() {
        return empty($this->_children);
    }
}

class xmlAttribute implements toText {
    private $_name;
    private $_value;

    function __construct($name, $value) {
        $this->_name = $name;
        $this->_value = $value;
    }

    public function name() {
        return $this->_name;
    }

    public function value() {
        return $this->_value;
    }

    public function text() {
        return $this->_name . '="' . htmlspecialchars($this->_text($this->_value)) . '"';
    }

    private function _text($content) {
        switch(gettype($content)) {
        case 'array':
            $text = '';
            foreach ($content as $element) {
                $text .= $this->_text($element);
            }
            return $text;
        case 'object':
            if (is_a($this->_value, 'toText')) {
                return $content->text();
            }
        default:
            return $content;
        }
    }
}

class attributes implements toText, container {
    private $_attributes = array();

    public function text() {
        $text = array();
        foreach ($this->_attributes as $xmlAttribute) {
            $text[] = $xmlAttribute->text();
        }
        return implode(' ', $text);
    }

    public function add(xmlAttribute $xmlAttribute) {
        $this->_attributes[$xmlAttribute->name()] = $xmlAttribute;

        return $this;
    }

    public function isempty() {
        return empty($this->_attributes);
    }
}

class tag extends xml {
    private $_children;
    private $_name;
    private $_attributes;

    function __construct(string $name) {
        $this->_name = $name;
        $this->_attributes = new attributes();
        $this->_children = new nodelist();
    }

    public function add() {
        foreach (func_get_args() as $node) {
            if (is_array($node)) {
                foreach ($node as $subnode) {
                    $this->add($subnode);
                }
            } else if (is_a($node, 'xmlAttribute')) {
                $this->_attributes->add($node);
            } else {
                $this->_children->add($node);
            }
        }
        return $this;
    }

    public function set(string $name, $value = null) {
        $this->_attributes->add(new xmlAttribute($name, $value));
        return $this;
    }

    public function attributes() {
        return $this->_attributes;
    }

    public function text() {
        return
            '<' . $this->_name .
            ($this->_attributes->isempty() ? '' : ' ' . $this->_attributes->text()) .
            ($this->_children->isempty() ? '/>' : '>' . $this->_children->text() . '</' . $this->_name . '>');
    }
}

class raw extends xml {
    private $_text;

    function __construct(string $text) {
        $this->_text = $text;
    }

    public function text() {
        return $this->_text;
    }
}

function makeTag($name, $content = null) {
    $tag = new tag($name);
    if (!is_null($content)) {
        $tag->add($content);
    }
    return $tag;
}

function makeRaw($content) {
    return new raw($content);
}

function makeAttribute($name, $value) {
    return new xmlAttribute($name, $value);
}

function makeTaglist() {
    return new nodelist();
}

function glueTags($glue, array $tags) {
    $glued = array();
    if (empty($tags)) return;
    $glued[] = array_shift($tags);
    foreach($tags as $tag) {
        $glued[] = $glue;
        $glued[] = $tag;
    }
    return $glued;
}

/srv/http/libs/html_utils.php, 2020-05-16 23:50:28 CEST
<?php
require_once('libs/xml.php');
require_once('libs/css.php');

function makeMeta($name, $content) {
    return makeTag('meta')->set('name', $name)->set('content', $content);
}

function makeHttpEquiv($name, $content) {
    return makeTag('meta')->set('http-equiv', $name)->set('content', $content);
}

function makeHyperlink($path, $content = null) {
    return makeTag('a', $content)->set('href', $path);
}

function makeAnchor($id) {
    return makeTag('a')->set('id', $id);
}

function makeRelLink($rel, $uri) {
    return makeTag('link')->set('rel', $rel)->set('href', $uri);
}

function makeCSSLink($path) {
    return makeRelLink('stylesheet', $path)->set('type', 'text/css');
}

function makeIconLink($path) {
    return makeRelLink('icon', $path);
}

function makePngIconLink($path) {
    return makeIconLink($path)->set('type', 'image/png');
}

function makeFieldset($title = null, $content = null) {
    return makeTag('fieldset', array(
        is_null($title) ? null : makeTag('legend', $title),
        $content,
    ));
}

function makeDiv($class = null, $content = null) {
    return makeTag('div', array(
        is_null($class) ? null : makeAttribute('class', $class),
        $content,
    ));
}

function makeSpan($content = null) {
    return makeTag('span', $content);
}

function makeTableHead(array $items) {
    $addTh = function($tag, $item) { return $tag->add(makeTag('th', $item)); };
    return makeTag('thead', array_reduce($items, $addTh, makeTag('tr')));
}

function makeTableRow(array $items) {
    $addTd = function($tag, $item) { return $tag->add(makeTag('td', $item)); };
    return array_reduce($items, $addTd, makeTag('tr'));
}

function makeTableRows(array $items, array $transforms) {
    $makeRow = function($item) use ($transforms) {
        $applyTransform = function($trans) use ($item) { return $trans($item); };
        return makeTableRow(array_map($applyTransform, $transforms));
    };
    return array_map($makeRow, $items);
}

function makeTableBody(array $items, array $transforms) {
    return makeTag('tbody')->add(makeTableRows($items, $transforms));
}

function makeComplexTable(array $items, array $transforms) {
    return makeTag('table', array(
        makeTableHead(array_column($transforms, 'title')),
        makeTableBody($items, array_column($transforms, 'transform')),
    ));
}

function makeTable(array $items, array $transforms) {
    return makeTag('table', makeTableBody($items, $transforms));
}

function makeUrl($url, array $params, $anchor = null) {
    if (!empty($params)) {
        $encodeParams = function ($key, $value) {
            return $value === '' ? urlencode($key) : urlencode($key) . '=' . urlencode($value);
        };
        $url .= '?' . implode('&', array_map($encodeParams, array_keys($params), $params));
    }

    if (!is_null($anchor)) {
        $url .= '#' . $anchor;
    }

    return empty($url) ? '?' : $url; // link to '?' to clear current query parameters
}

function makeLink($baseUrl, $label = null, array $params = array(), $anchor = null) {
    return makeHyperlink(makeUrl($baseUrl, $params, $anchor), $label);
}

function makeAnchorLink($anchor, $label = null) {
    return makeLink('', $label, array(), $anchor); 
}

function makeExternalLink($baseUrl, $label = null, array $params = array()) {
    return makeLink($baseUrl, $label, $params)->set('target', '_blank');
}

function makeScript($content = null) {
    return makeTag('script', $content);
}

function makeScriptLink($url) {
    return makeScript()->set('src', $url);
}

function makeImg($src, $alt = null) {
    $tag = makeTag('img')->set('src', $src);
    if ($alt) {
        $tag->set('alt', $alt);
    }
    return $tag;
}

function makeListItems(array $items) {
    return array_map(function($item) { return makeTag('li', $item); }, $items);
}

function makeList(array $items) {
    return makeTag('ul', makeListItems($items));
}

function makeNumberedList(array $items) {
    return makeTag('ol', makeListItems($items));
}

function makeCssTag(css $css) {
    return makeTag('style')
        //->set('type', 'text/css') // not needed according to validator.w3.org and https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style
        ->add($css);
}

function makeStyleAttribute(style $style) {
    return makeAttribute('style', $style->text());
}

function makeSpaceList(array $items) {
    return glueTags(' ', $items);
}

function makeCommaList(array $items) {
    return glueTags(', ', $items);
}

function makeSerialList($delim, array $items) {
    if (count($items) <= 1) {
        return $items;
    }
    if (count($items) == 2) {
        return makeSpaceList(array($items[0], $delim, $items[1]));
    }
    // With or without serial comma? https://en.wikipedia.org/wiki/Serial_comma
    $last = array_pop($items);
    //return makeSpaceList(array(makeCommaList($items), $delim, $last)); // no serial comma
    $items[] = makeSpaceList(array($delim, $last));
    return makeCommaList($items);
}

function makeAndList(array $items) {
    return makeSerialList('and', $items);
}

function makeOrList(array $items) {
    return makeSerialList('or', $items);
}

function makeSentence(array $items) {
    return array(makeSpaceList($items), '.');
}

function makeBraces($data) {
    return array('(', $data, ')');
}

function makeLines(array $items) {
    return glueTags(makeTag('br'), $items);
}

function makeParagraphs(array $items) {
    $br = makeTag('br');
    return glueTags(array($br, $br), $items);
    //return array_map(function($item) { return makeTag('p', $item); }, $items);
}

function makeQuote($content) {
    return array('“', $content, '”');
}

/srv/http/libs/css.php, 2016-10-16 12:21:33 CEST
<?php
require_once('libs/doc.php');
require_once('libs/style.php');

class css extends doc {
    private $_entries = array();

    public function __construct() {
        parent::__construct('text/css');
    }

    public function add($selector, $style) {
        if (!array_key_exists($selector, $this->_entries)) {
            $this->_entries[$selector] = makeStyle();
        }
        $this->_entries[$selector]->add($style);
        return $this;
    }

    public function text() {
        $text = '';
        foreach ($this->_entries as $selector => $style) {
            $text .=  $selector . '{' . $style->text() . '}';
        }
        return $text;
    }
}

function makeCss() {
    return new css();
}

/srv/http/libs/style.php, 2016-07-14 11:23:33 CEST
<?php
require_once('libs/text.php');

class styleAttribute implements toText {
    private $_name;
    private $_value;

    public function __construct($name, $value) {
        $this->_name = $name;
        $this->_value = $value;
    }

    public function name() {
        return $this->_name;
    }

    public function value() {
        return $this->_value;
    }

    public function text() {
        return $this->_name . ':' . $this->_value;
    }
}

class style implements toText {
    private $_attributes = array();

    public function add($style) {
        switch (gettype($style)) {
        case 'object':
            switch (get_class($style)) {
            case 'style':
                $this->addStyle($style);
                break;
            case 'styleAttribute':
                $this->addAttribute($style);
                break;
            }
            // throw error?
            break;
        case 'array':
            foreach ($style as $child) {
                $this->add($child);
            }
            break;
        }
        return $this;
    }

    public function set($name, $value) {
        $this->add(new styleAttribute($name, $value));
        return $this;
    }

    public function addStyle(style $style) {
        foreach ($style->_attributes as $attribute) {
            $this->addAttribute($attribute);
        }
        return $this;
    }

    public function addAttribute(styleAttribute $attribute) {
        $this->_attributes[$attribute->name()] = $attribute;
        return $this;
    }

    public function text() {
        return implode(';', array_map(function($attribute) { return $attribute->text(); }, $this->_attributes));
    }
}

function makeStyle() {
    return new style();
}

/srv/http/libs/http.php, 2017-01-21 19:54:14 CET
<?php
require_once('libs/doc.php');

class http {
    private $_headers;

    private function makeHeader($name, $params = null) {
        if (empty($params)) {
            return $name;
        }
        if (is_array($params)) {
            $params = implode(';', $params);
        }
        return $name . ':' . $params;
    }

    public function addHeader($name, $params) {
        $this->_headers[] = $this->makeHeader($name, $params);
        return $this;
    }

    private function addDocHeaders(doc $doc) {
        $this
            ->addHeader('Content-type', array(
                $doc->type(),
                'charset=' . $doc->encoding(),
            ))
            ;

        if ($language = $doc->language()) {
            $this->addHeader('Content-Language', $language);
        }

        if ($name = $doc->name()) {
            $this->addHeader('Content-Disposition', array(
                'attachment',
                //($inline ? 'inline' : 'attachment'),
                //'filename="' . $filename . '"', // unescaped, unsafe?
                //'filename=' . rawurlencode($filename), // url-encoded, for IE?
                'filename*=UTF-8\'\'' . rawurlencode($name), // special UTF-8 format supported by modern browsers, but not cURL :/
            ))
            ;
        }
        return $this;
    }

    public function render(doc $doc) {
        $content = $doc->text();
        $etag = sha1($content);
        //header('etag:' . $etag);
        //header('x-compute-time-ms:' . round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 100000) / 100) ;

        $this
            ->addHeader('etag', $etag)
            ->addHeader('x-compute-time-ms', round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 100000) / 100)
            ->addDocHeaders($doc)
            ->addHeader('Access-Control-Allow-Origin', '*')
            //->addHeader('Cache-Control', 'public, max-age=120')
            //->addHeader('Cache-Control', 'public')
            //->addHeader('Pragma', 'cache')
            ;

        foreach ($this->_headers as $header) { header($header); }

        if (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag) {
            header('HTTP/1.0 304 Not Modified');
            return;
        }

        ob_start('ob_gzhandler');
        echo $content;
        ob_end_flush();
    }
}

function makeHttp() {
    return new http;
}

/srv/http/debug.php, 2017-05-01 18:03:21 CEST
<?php
require_once('libs/doc.php');

function makeDebuggable(doc $doc, $title, $style = null) {
    $type = $doc->type();
    if (!$style) $style = $type;

    if (array_key_exists('debug', $_GET)) {
        $_GET = array();
        require_once('debugdoc.php');
        $uri = makeUrl('http://' . $_SERVER['HTTP_HOST'] . $_SERVER['DOCUMENT_URI'], $_GET);
        return makeDebugdoc($title)
            ->addFileBlock($title, $style, $doc)
            ->addSources()
            ->addValidators($uri, $type)
            ->addServerVars()
            ->build()
            ->highlight()
            ;
    }

    return $doc;
}

/srv/http/radio/radio_data.php, 2024-04-03 17:35:04 CEST
<?php

return array(
    'music' => array(
        array(
            'title' => 'ORF: FM4',
            //'link' => 'http://mp3stream1.apasf.apa.at:8000/', 'codec' => 'MP3', 'bitrate' => '160',
            //'link' => 'http://mp3fm4.apasf.sf.apa.at/', 'codec' => 'MP3', 'bitrate' => '160',
            //'link' => 'http://mp3fm4.apasf.sf.apa.at/?type=http', 'codec' => 'MP3', 'bitrate' => '160',
            'link' => 'https://orf-live.ors-shoutcast.at/fm4-q2a', 'codec' => 'MP3', 'bitrate' => '192',
            //'link' => 'mms://apasf.apa.at/fm4_live_worldwide', 'codec' => 'WMA', 'bitrate' => '128',
            'website' => 'http://fm4.orf.at/',
            'extra' => 'Pop',
            'location' => 'Vienna, Austria',
            'info' => 'originally US army base radio station for the soldiers that were stationed in Austria after the war',
        ),
        array(
            'title' => '313.FM',
            'link' => 'http://icecast.ofdoom.com:8000/burst.mp3', 'codec' => 'MP3', 'bitrate' => '128',
            //'link' => 'http://icecast.ofdoom.com:8000/burst.ogg', 'codec' => 'Vorbis', 'bitrate' => '112',
            //'link' => 'http://icecast.ofdoom.com:8000/burst.opus', 'codec' => 'Opus', 'bitrate' => '64',
            'website' => 'http://313.fm/',
            'extra' => 'House',
            'location' => 'Detroit, USA',
            'info' => 'Detroit\'s Electronic Music Station',
        ),
        array(
            'title' => 'Studio Brussel',
            //'link' => 'http://mp3.streampower.be/stubru-high.mp3', 'codec' => 'MP3', 'bitrate' => '128',
            //'link' => 'http://mp3.streampower.be/stubru.aac', 'codec' => 'AAC', 'bitrate' => '128',
            'link' => 'http://icecast.vrtcdn.be/stubru.aac', 'codec' => 'AAC-LC', 'bitrate' => '128',
            //'link' => 'https://live-radio.lwc.vrtcdn.be/groupc/live/f404f0f3-3917-40fd-80b6-a152761072fe/live.isml/live-audio=128000.m3u8', 'codec' => 'AAC-LC', 'bitrate' => '128',
            //'link' => 'https://live-radio.lwc.vrtcdn.be/groupc/live/f404f0f3-3917-40fd-80b6-a152761072fe/live.isml/live-audio=64000.m3u8', 'codec' => 'HE-AAC', 'bitrate' => '64',
            'website' => 'https://stubru.be/',
            'extra' => 'Pop',
            'location' => 'Brussels, Belgium',
            'info' => 'alternative music',
        ),
        array(
            'title' => 'Studio Brussel — Bruut',
            'link' => 'http://icecast.vrtcdn.be/stubru_bruut.aac', 'codec' => 'AAC-LC', 'bitrate' => '128',
            'website' => 'https://stubru.be/bruut',
            'extra' => 'Rock',
            'location' => 'Brussels, Belgium',
            'info' => '',
        ),
        array(
            'title' => 'Studio Brussel — Hooray',
            'link' => 'http://icecast.vrtcdn.be/stubru_hiphophooray.aac', 'codec' => 'AAC-LC', 'bitrate' => '128',
            'website' => 'http://stubru.be/hooray',
            'extra' => 'Hiphop',
            'location' => 'Brussels, Belgium',
            'info' => '',
        ),
        array(
            'title' => 'Studio Brussel — De Tijdloze',
            'link' => 'http://icecast.vrtcdn.be/stubru_tijdloze.aac', 'codec' => 'AAC-LC', 'bitrate' => '128',
            'website' => 'http://stubru.be/detijdloze',
            'extra' => 'Classic Rock',
            'location' => 'Brussels, Belgium',
            'info' => '',
        ),
        array(
            'title' => 'Studio Brussel — UNTZ',
            'link' => 'http://icecast.vrtcdn.be/stubru_untz.aac', 'codec' => 'AAC-LC', 'bitrate' => '128',
            'website' => 'https://stubru.be/untz',
            'extra' => 'Electronic',
            'location' => 'Brussels, Belgium',
            'info' => '',
        ),
        array(
            'title' => 'YleX',
            //'link' => 'https://yleuni-f.akamaihd.net/i/yleliveradiohd_2@113879/master.m3u8', 'codec' => 'AAC-LC', 'bitrate' => '256 (VBR)',
            //'link' => 'https://yleuni-f.akamaihd.net/i/yleliveradiohd_2@113879/index_256_a-b.m3u8', 'codec' => 'AAC-LC', 'bitrate' => '256',
            'link' => 'https://yleuni-f.akamaihd.net/i/yleliveradiohd_2@113879/index_256_a-p.m3u8', 'codec' => 'AAC-LC', 'bitrate' => '256',
            'website' => 'https://areena.yle.fi/',
            'extra' => 'Pop',
            'location' => 'Helsinki, Finland',
            'info' => '',
        ),
        array(
            'title' => 'DR P3',
            //'link' => 'http://drradio3-lh.akamaihd.net/i/p3_9@143506/master', 'codec' => 'AAC-LC', 'bitrate' => '256 (VBR)',
            'link' => 'http://drradio3-lh.akamaihd.net/i/p3_9@143506/index_256_a-b.m3u8', 'codec' => 'AAC-LC', 'bitrate' => '256',
            'website' => 'https://www.dr.dk/',
            'extra' => 'Pop',
            'location' => 'Copehagen, Denmark',
            'info' => '',
        ),
        array(
            'title' => 'Couleur3',
            'link' => 'http://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8', 'codec' => 'HE-AAC', 'bitrate' => '96',
            'website' => 'http://www.rts.ch/couleur3/',
            'extra' => 'Pop',
            'location' => 'Geneva, Switserland',
            'info' => '',
        ),
        array(
            'title' => 'SRF3',
            //'link' => 'http://stream.srg-ssr.ch/m/drs3/mp3_128', 'codec' => 'MP3', 'bitrate' => '128',
            'link' => 'http://stream.srg-ssr.ch/m/drs3/aacp_96', 'codec' => 'HE-AAC', 'bitrate' => '96',
            'website' => 'http://www.srf.ch/radio-srf-3',
            'extra' => 'Pop',
            'location' => 'Zürich, Switserland',
            'info' => makeSpaceList(array('youth oriented music from', makeQuote('Schweizer Radio und Fernsehen'))),
        ),
        array(
            'title' => 'NRK P3',
            'link' => 'http://lyd.nrk.no:80/nrk_radio_p3_aac_h', 'codec' => 'AAC-LC', 'bitrate' => '128',
            'website' => 'http://p3.no/',
            'extra' => 'Pop',
            'location' => 'Trondheim, Norway',
            'info' => makeSpaceList(array('youth oriented music channel from', makeQuote('Norsk rikskringkasting'))),
        ),
        /*
        array(
            'title' => 'WJHU',
            'link' => 'http://128.220.253.157:80/wjhuradio.opus', 'codec' => 'Opus', 'bitrate' => '64', //array(makeExternalLink('https://www.opus-codec.org/', 
            'website' => 'http://www.wjhuradio.org/',
            'extra' => 'Pop',
            'location' => 'Baltimore, USA',
            'info' => 'student radio station from Johns Hopkins University',
        ),
         */
        array(
            'title' => 'Radio NABA',
            //'link' => 'http://nabamp0.latvijasradio.lv:8016/', 'codec' => 'MP3', 'bitrate' => '128',
            //'link' => 'http://muste.radio.org.lv:1935/shoutcast/naba.stream/playlist.m3u8', 'codec' => 'HE-AAC', 'bitrate' => '112',
            //'link' => 'rtsp://muste.radio.org.lv/shoutcast/mp4:naba.stream', 'codec' => 'HE-AAC', 'bitrate' => '112',
            //'link' => 'http://muste.radio.org.lv/shoutcast/mp4:naba.stream/playlist.m3u8', 'codec' => 'HE-AAC', 'bitrate' => '112',
            //'link' => 'https://muste.radio.org.lv/shoutcast/mp4:naba.stream/playlist.m3u8', 'codec' => 'HE-AAC', 'bitrate' => '112',
            'link' => 'https://5a44e5b800a41.streamlock.net/shoutcast/mp4:naba.stream/playlist.m3u8', 'codec' => 'HE-AAC', 'bitrate' => '112',
            'website' => 'http://www.naba.lv/',
            'extra' => 'Alt',
            'location' => 'Riga, Latvia',
            'info' => '',
        ),
        array(
            'title' => 'Triple J',
            //'link' => 'http://shoutmedia.abc.net.au:10326', 'codec' => 'AAC', 'bitrate' => '48',
            //'link' => 'http://shoutmedia.abc.net.au:10426', 'codec' => 'MP3', 'bitrate' => '96',
            //'link' => 'http://live-radio01.mediahubaustralia.com:8048/', 'codec' => 'MP3', 'bitrate' => '96',
            //'link' => 'http://live-radio01.mediahubaustralia.com/2TJW/mp3/;*.mp3', 'codec' => 'MP3', 'bitrate' => '96',
            //'link' => 'http://live-radio01.mediahubaustralia.com/2TJW/aac/;*.aac', 'codec' => 'AAC-LC', 'bitrate' => '64',
            'link' => 'https://abcradiolivehls-lh.akamaihd.net/i/triplejnsw_1@327300/master.m3u8', 'codec' => 'AAC-LC', 'bitrate' => '96',
            'website' => 'http://www.abc.net.au/triplej/',
            'extra' => 'Pop',
            'location' => 'Sydney, Australia',
            'info' => 'Thanks Andrew :)',
        ),
        //http://www.radiofrance.fr/sites/default/files/pages_uploaded_files/radio_france_-_adresses_flux_mp3_-_mai_2015_0.pdf
        array(
            'title' => 'Fip',
            'link' => 'https://stream.radiofrance.fr/fip/fip_hifi.m3u8', 'codec' => 'MP3', 'bitrate' => '192',
            'website' => 'https://www.radiofrance.fr/fip',
            'extra' => 'Alternative',
            'location' => 'Paris, France',
            'info' => '',
        ),
        array(
            'title' => 'Fip Rock',
            'link' => 'https://stream.radiofrance.fr/fiprock/fiprock_hifi.m3u', 'codec' => 'MP3', 'bitrate' => '192',
            'website' => 'https://www.radiofrance.fr/fip',
            'extra' => 'Alternative/Rock',
            'location' => 'Paris, France',
            'info' => '',
        ),
        array(
            'title' => 'Mouv\'',
            //'link' => 'http://direct.mouv.fr/live/mouv-midfi.mp3', 'codec' => 'MP3', 'bitrate' => '128',
            'link' => 'https://stream.radiofrance.fr/mouv/mouv_hifi.m3u8', 'codec' => 'MP3', 'bitrate' => '192',
            'website' => 'http://www.mouv.fr/',
            'extra' => 'Pop',
            'location' => 'Paris, France',
            'info' => 'youth oriented music channel from Radio France',
        ),
        array(
            'title' => 'Mouv\' Xtra',
            'link' => 'http://direct.mouv.fr/live/mouvxtra-midfi.mp3', 'codec' => 'MP3', 'bitrate' => '128',
            'website' => 'http://www.mouv.fr/',
            'extra' => 'Hiphop',
            'location' => 'Paris, France',
            'info' => 'hiphop sister channel of Mouv\'',
        ),
        //http://www.einslive.de/einslive/on-air/webradio/
        /*
        array(
            'title' => 'BBC Radio 1',
            'link' => 'http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_one.m3u8', 'codec' => 'HE-AAC', 'bitrate' => '96',
            'website' => '',
            'extra' => 'Pop',
            'location' => 'London, UK',
        ),
         */
        array(
            'title' => 'BBC Radio 1Xtra',
            'link' => 'http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/llnw/bbc_1xtra.m3u8', 'codec' => 'HE-AAC', 'bitrate' => '96',
            'website' => 'http://www.bbc.co.uk/1xtra',
            'extra' => 'Pop',
            'location' => 'London, UK',
            'info' => 'modern music from the BBC',
        ),
        array(
            'title' => 'Sveriges Radio P3',
            'link' => 'http://http-live.sr.se/p3-aac-96', 'codec' => 'HE-AAC', 'bitrate' => '96',
            'website' => 'http://sverigesradio.se/p3/',
            'extra' => 'Pop',
            'location' => 'Stockholm, Sweden',
            'info' => 'youth oriented music from the people who brought you IKEA'
        ),
        array(
            'title' => '1LIVE',
            //'link' => 'http://1live.akacast.akamaistream.net/7/706/119434/v1/gnl.akacast.akamaistream.net/1live', 'codec' => 'MP3', 'bitrate' => '128',
            'link' => 'https://1liveuni-lh.akamaihd.net/i/1LIVE_HDS@179577/index_1_a-b.m3u8', 'codec' => 'AAC-LC', 'bitrate' => '96',
            'website' => 'http://www.einslive.de/',
            'extra' => 'Pop',
            'location' => 'Cologne, Germany',
            'info' => 'popular music from WDR',
        ),
        array(
            'title' => '3FM',
            'link' => 'http://icecast.omroep.nl/3fm-bb-aac', 'codec' => 'HE-AAC', 'bitrate' => '64',
            'website' => 'http://www.3fm.nl/',
            'extra' => 'Pop',
            'location' => 'Hilversum, Netherlands',
            'info' => 'alternative music',
        ),
        array(
            'title' => 'Radio Activa',
            //'link' => 'http://14783.live.streamtheworld.com/RADIO_ACTIVAAAC_SC', 'codec' => 'AAC', 'bitrate' => '32',
            //'link' => 'http://9443.live.streamtheworld.com/RADIO_ACTIVAAAC.aac', 'codec' => 'AAC', 'bitrate' => '32',
            //'link' => 'http://18553.live.streamtheworld.com/RADIO_ACTIVAAAC.aac', 'codec' => 'HE-AACv2', 'bitrate' => '32',
            'link' => 'https://27573.live.streamtheworld.com/RADIO_ACTIVAAAC.aac', 'codec' => 'HE-AAC', 'bitrate' => '64',
            'website' => 'http://www.radioacktiva.com/',
            'extra' => 'Rock',
            'location' => 'Bogota, Colombia',
            'info' => 'Planeta Rock',
        ),
        array(
            'title' => 'Серебряный Дождь',
            //'link' => 'http://85.21.192.13:8000/silver128b.mp3', 'codec' => 'MP3', 'bitrate' => '128',
            //'link' => 'http://213.59.4.27:8000/silver128.mp3', 'codec' => 'MP3', 'bitrate' => '128',
            'link' => 'http://213.59.4.27:8000/silver64.aac', 'codec' => 'HE-AAC', 'bitrate' => '64',
            //'link' => 'http://radiosilver.corbina.net:8000/silver128a.mp3', 'codec' => 'MP3', 'bitrate' => '128',
            'website' => 'http://www.silver.ru/',
            'extra' => 'Alt',
            'location' => 'Moscow, Russia',
            'info' => 'Silver Rain Radio',
        ),
        array(
            'title' => '5FM',
            //'link' => 'rtsp://196.33.130.72:1935/5fm/5fm.stream', 'codec' => 'AAC', 'bitrate' => '54',
            //'link' => 'http://radio-int-edge-eu01-sc.antfarm.co.za:9034/5fm', 'codec' => 'HE-AAC', 'bitrate' => '64',
            //'link' => 'http://colonya.antfarm.co.za/ant-lre-sabc/9f38102608b345dcb53c1db0bd57f2d3/playlist.m3u8', 'codec' => 'AAC-LC', 'bitrate' => '64',
            'link' => 'http://25483.live.streamtheworld.com/5FMAAC.aac', 'codec' => 'HE-AAC', 'bitrate' => '64',
            'website' => 'http://www.5fm.co.za/',
            'extra' => 'Pop',
            'location' => 'Johannesburg, South Africa',
            'info' => 'from Thosha',
        ),
        /*
        array(
            'title' => 'Arrow Classic Rock',
            'link' => 'http://91.221.151.155:80/', 'codec' => 'MP3', 'bitrate' => '128',
            //'link' => 'http://91.221.151.178:8109/', 'codec' => 'MP3', 'bitrate' => '128',
            //'link' => 'http://91.221.151.156:80/', 'codec' => 'AAC', 'bitrate' => '96',
            //'link' => 'http://91.221.151.178:9109/', 'codec' => 'AAC', 'bitrate' => '96',
            'website' => 'http://www.arrow.nl/',
            'extra' => 'Rock',
            'location' => 'Rotterdam, Netherlands',
            'info' => 'classic rock',
        ),
         */
        //http://live-radio01.mediahubaustralia.com/2TJW/mp3/index.html?sid=1
        //http://live-radio01.mediahubaustralia.com/2TJW/aac/index.html?sid=1
    ),
    'talk' => array(
        array(
            'title' => 'BBC Radio 4',
            'link' => 'http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/llnw/bbc_radio_fourfm.m3u8', 'codec' => 'HE-AAC', 'bitrate' => '96',
            'website' => 'http://www.bbc.co.uk/radio4',
            'extra' => 'Talk, drama',
            'location' => 'London, UK',
        ),
        array(
            'title' => 'Deutschlandfunk',
            //'link' => 'http://dradio-ogg-dwissen-l.akacast.akamaistream.net/7/192/135496/v1/gnl.akacast.akamaistream.net/dradio_ogg_dwissen_l', 'codec' => 'Vorbis', 'bitrate' => '128', //array(makeExternalLink('http://www.vorbis.com/', 
            //'link' => 'http://st01.dlf.de/dlf/01/104/ogg/stream.ogg', 'codec' => 'Vorbis', 'bitrate' => '104', //array(makeExternalLink('http://www.vorbis.com/', 
            'link' => 'https://st03.sslstream.dlf.de/dlf/03/high/opus/stream.opus', 'codec' => 'Opus', 'bitrate' => '64', //array(makeExternalLink('http://www.vorbis.com/', 
            'website' => 'http://dradiowissen.de/',
            'extra' => 'Talk',
            'location' => 'Cologne, Germany',
            'info' => 'educational channel with youth oriented music',
        ),
        array(
            'title' => 'France Inter',
            'link' => 'http://direct.franceinter.fr/live/franceinter-midfi.mp3', 'codec' => 'MP3', 'bitrate' => '128',
            'website' => 'http://www.franceinter.fr/',
            'extra' => 'Talk',
            'location' => 'Paris, France',
            'info' => 'It is a “generalist” station, aiming to provide a wide national audience with a full service of news and spoken-word programming',
        ),
        array(
            'title' => 'France Info',
            'link' => 'http://direct.franceinfo.fr/live/franceinfo-midfi.mp3', 'codec' => 'MP3', 'bitrate' => '128',
            'website' => 'http://www.franceinfo.fr/',
            'extra' => 'News',
            'location' => 'Paris, France',
            'info' => 'News',
        ),
        array(
            'title' => 'Cadena SER',
            //'link' => 'http://5293.live.streamtheworld.com:443/CADENASERAAC_SC', 'codec' => 'AAC', 'bitrate' => '48',
            //'link' => 'http://20403.live.streamtheworld.com/CADENASERAAC.aac', 'codec' => 'HE-AACv2', 'bitrate' => '48',
            'link' => makeUrl('https://20073.live.streamtheworld.com/CADENASERAAC.aac', array('csegid' => 12000)), 'codec' => 'HE-AACv2', 'bitrate' => '48',
            'website' => 'http://www.cadenaser.com/',
            'extra' => 'Talk, News',
            'location' => 'Madrid, Spain',
            'info' => 'Escucha con nosotros la vida',
        ),
    ),
);

/srv/http/debugdoc.php, 2018-10-18 18:00:40 CEST
<?php
require_once('highlightdoc.php');

class debugdoc extends highlightdoc {
    protected $_debuggers = array();

    function __construct($title) {
        parent::__construct($title, '/style-debug/');
    }

    protected function addDebugger($id, $title, $tag) {
        $this->_debuggers[$id][] = array('id' => $id, 'title' => $title, 'tag' => $tag);
        return $this;
    }

    protected function makeFileBlock($title, $style, $content) {
        return makeFieldset($title, $this->makeHighlight($style, $content));
    }

    public function addFileBlock($title, $style, $content) {
        return $this->addDebugger('file', $title, $this->makeFileBlock($title, $style, $content));
    }

    protected function makeSources() {
        $blocks = array(
            makeFieldset('source files', $table = makeTag('table')),
        );
        foreach(get_included_files() as $id => $file) {
            $anchor = 'source' . $id;
            $table->add(makeTableRow(array(makeAnchorLink($anchor, $file))));
            $modified = date('Y-m-d H:i:s T', filemtime($file));
            $blocks[] = array(
                makeAnchor($anchor),
                $this->makeFileBlock($file . ', ' . $modified, 'php', file_get_contents($file)),
            );
        }
        return makeLines($blocks);
    }

    public function addSources() {
        return $this->addDebugger('sources', 'source files', $this->makeSources());
    }

    private function makeValidators($uri, $type) {
        $validators = array();
        //$validators[] = $type;
        if (in_array($type, array('text/css', 'application/xhtml+xml'))) {
            $validators[] = makeExternalLink('https://jigsaw.w3.org/css-validator/validator', 'W3C CSS Validator', array('uri' => $uri));
        }
        if (in_array($type, array('application/atom+xml'))) {
            $validators[] = makeExternalLink('https://validator.w3.org/feed/check.cgi', 'W3C Feed Validator', array('url' => $uri));
        }
        if (in_array($type, array('application/xhtml+xml'))) {
            $validators[] = makeExternalLink('https://validator.w3.org/nu/', 'Nu Html Checker', array('doc' => $uri));
            $validators[] = makeExternalLink('https://validator.w3.org/i18n-checker/check', 'W3C Internationalization Checker', array('uri' => $uri));
            //makeExternalLink('https://ssldecoder.org/', 'SSL Decoder', array('host' => $uri)),
            //makeExternalLink('https://validator.w3.org/checklink', 'Link Checker', array('uri' => $uri)),
            //makeExternalLink('https://validator.w3.org/mobile/check', 'Mobile Checker', array('docAddr' => $uri)),
            //makeExternalLink('https://html5.validator.nu/', 'html5.validator.nu', array('doc' => $uri)),
        }
        return empty($validators) ? null : makeFieldset('validators', makeTable($validators, array(function($tag) { return $tag; })));
    }

    public function addValidators($uri, $type) {
        $validators = $this->makeValidators($uri, $type);
        return $validators ? $this->addDebugger('validators', 'validators', $validators) : $this;
    }

    public function addServerVars() {
        $title = 'server variables';
        $tag = makeFieldset($title, $table = makeTag('table'));
        foreach ($_SERVER as $key => $value) {
            $table->add(makeTableRow(array(makeTag('strong', $key), $value)));
        }
        return $this->addDebugger('vars', $title, $tag);
    }

    public function build() {
        $flatten = array();
        foreach ($this->_debuggers as $id => $debuggers) {
            if (count($debuggers) == 1) {
                $flatten[] = array('id' => $id, 'debugger' => $debuggers[0]);
            } else {
                foreach ($debuggers as $num => $debugger) {
                    $flatten[] = array('id' => ($id . $num), 'debugger' => $debugger);
                }
            }
        }
        $menu = makeFieldset('debug', makeTable($flatten, array(function($debugger) {
            return makeLink('', $debugger['debugger']['title'], array(), $debugger['id']);
        })));
        $blocks = array();
        $blocks[] = $menu;
        foreach($flatten as $debugger) {
            $blocks[] = array(
                makeAnchor($debugger['id']),
                $debugger['debugger']['tag'],
            );
        }
        $this->body()->add(makeLines($blocks));
        return $this;
    }
}

function makeDebugdoc($title = 'debug') {
    return new debugdoc($title);
}

/srv/http/highlightdoc.php, 2016-04-03 16:31:18 CEST
<?php
require_once('basedoc.php');
require_once('highlight.php');

class highlightdoc extends basedoc {
    use highlight;
}

function makeHighlightdoc($title) {
    return new highlightdoc($title);
}

/srv/http/highlight.php, 2016-10-31 23:24:15 CET
<?php
require_once('libs/html_utils.php');

trait highlight {
    private $_highlight = false;

    public function makeHighlight($class, $content) {
        $this->_highlight = true;
        return makeTag('pre', makeTag('code', $content)->set('class', $class))->set('class', 'wrap');
    }

    public function highlight() {
        if (!$this->_highlight) {
            return;
        }
        require_once('libs/css.php');
        $css = makeCss()
            //->add('.hljs', makeStyle()->set('background', 'inherit'))
            //->add('.hljs', makeStyle()->set('color', 'Black'))
            ->add('.hljs-variable,.hljs-attr,.hljs-attribute', makeStyle()->set('color', 'DarkBlue'))
            ->add('.hljs-title,.hljs-selector-tag,.hljs-selector-class,.hljs-meta,.hljs-pi', makeStyle()->set('font-weight', 'bold'))
            ->add('.hljs-keyword,.hljs-name,.hljs-built_in', makeStyle()->set('color', 'DarkBlue')->set('font-weight', 'bold'))
            ->add('.hljs-value,.hljs-string,.hljs-number', makeStyle()->set('color', 'DarkRed'))
            ->add('.hljs-comment', makeStyle()->set('color', 'DarkSlateGray')->set('font-style', 'italic'))
            //->add('.hljs-function', makeStyle()->set('font-style', 'italic'))
            ->add('pre.wrap', makeStyle()->set('white-space', 'pre-wrap'))
            ;
        //$highlightLocation = '//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.8.0/';
        //$highlightLocation = '//cdn.jsdelivr.net/highlight.js/8.8.0/';
        //$highlightLocation = '//cdn.jsdelivr.net/highlight.js/9.1.0/';
        $this->head()->add(array(
            //makeCSSLink($highlightLocation . 'styles/default.min.css'),
            makeCssTag($css),
        ));
        $this->body()->add(array(
            //makeScriptLink($highlightLocation . 'highlight.min.js'),
            makeScriptLink('/highlight.pack.js'),
            //makeScript('hljs.initHighlightingOnLoad();'),
            makeScriptLink('/highlight_init.js'),
        ));
        return $this;
    }
}

validators
W3C CSS Validator
Nu Html Checker
W3C Internationalization Checker

server variables
USERhttp
HOME/srv/http
HTTP_CONNECTIONKeep-Alive
HTTP_HOSTmarcoen.dev
HTTP_ACCEPT_ENCODINGbr,gzip
HTTP_ACCEPT_LANGUAGEen-US,en;q=0.5
HTTP_ACCEPTtext/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
HTTP_USER_AGENTCCBot/2.0 (https://commoncrawl.org/faq/)
REDIRECT_STATUS200
SERVER_NAMElocalhost
SERVER_PORT8000
SERVER_ADDR::1
REMOTE_PORT54262
REMOTE_ADDR::ffff:3.230.173.188
SERVER_SOFTWAREnginx/1.25.5
GATEWAY_INTERFACECGI/1.1
REQUEST_SCHEMEhttp
SERVER_PROTOCOLHTTP/1.1
DOCUMENT_ROOT/srv/http
DOCUMENT_URI/radio/index.php
REQUEST_URI/radio/?debug
SCRIPT_NAME/radio/index.php
CONTENT_LENGTH
CONTENT_TYPE
REQUEST_METHODGET
QUERY_STRINGdebug
SCRIPT_FILENAME/srv/http/radio/index.php
FCGI_ROLERESPONDER
PHP_SELF/radio/index.php
REQUEST_TIME_FLOAT1718995183.8144
REQUEST_TIME1718995183