EPG server (XMLTV)

A separate built-in HTTP server in Perfect Streamer serves the full XMLTV for a given set of channels. The endpoint is intended for middleware and players that store the programme guide locally and refresh it infrequently, as a whole file — typically once or twice a day.

The server and its clients are configured under Configuration/EPG/EPG Server.

URL and authentication

The service is served by a separate HTTP server epg-server (not the one for /data/*). By default it listens on ports 10444 (HTTP) and 10445 (HTTPS); the ports and SSL are configured under /config/epg-server.

Routes:

URL

Content-Type

Behavior

GET /xmltv

text/xml

With Accept-Encoding: gzip the response is compressed on the fly (Content-Encoding: gzip), otherwise it is returned as plain XML.

GET /xmltv.gz

application/octet-stream

Always returns a gzip stream with Content-Disposition: inline; filename="xmltv.xml.gz" — convenient to save as a file.

Three authentication methods are supported:

  • HTTP Digest (preferred) — account from /config/epg-server/login.

  • URL parameters — ?l=<login>&p=<password> (synonyms: login=…, password=…).

  • Loopback — a request from 127.0.0.1 is handled anonymously. Convenient for scripts deployed on the same machine.

Warning

A login and password in the URL end up in the reverse-proxy access logs and in the browser history. For public XMLTV distribution use HTTP Digest or accept requests only from private addresses.

Access by channel-set

Each epg-server/login account is bound to one channel-set from /config/epg-channel-set. The EPG in the response contains only the channels that belong to the specified set. This lets a single PSS deployment serve different XMLTV to different operators/middleware.

Basic setup in the UI:

  1. Under Configuration/EPG/EPG Channel Sets, create a channel group and assign the desired channels to it at the EPG sources.

  2. Under Configuration/EPG/EPG Server Clients, create an account and bind the channel group to it. Without a channel-set binding the client gets an empty XMLTV.

Additional restrictions for a login:

  • ip-addr — if set and not a wildcard, a request from a different IP gets 403 Forbidden.

  • limit-day — Unix epoch sec after which the account stops being served (403 Forbidden). Convenient for a subscription model.

  • pause — temporarily disable a login without deleting it.

Response format

Response body is an XMLTV document rooted at <tv>. The structure follows the commonly used XMLTV DTD schema:

<?xml version="1.0" encoding="utf-8"?>
<tv source-info-name="..." source-info-url="...">
  <channel id="channel.one">
    <display-name lang="en">Channel One</display-name>
    <icon src="https://.../channel-one.png"/>
  </channel>
  ...
  <programme start="20260504060000 +0300"
             stop ="20260504070000 +0300"
             channel="channel.one">
    <title lang="en">Morning News</title>
    <desc  lang="en">Overview of the day's events</desc>
    <rating system="MPAA"><value>PG</value></rating>
  </programme>
  ...
</tv>

Field notes:

  • The source-info-name and source-info-url attributes of the <tv> root are filled from the EPG Source Name and EPG Source URL fields under Configuration/EPG/EPG Server.

  • The start and stop attributes use the YYYYMMDDhhmmss ±zone format (the time zone comes from the channel’s time_zone field).

  • A <programme> may contain several <title>/<desc> for different languages. The lang attribute is empty when the source EPG language id could not be mapped to the dictionary (the entry still appears in the output).

  • Channels with a conflicting channel_id (if the same id came from multiple sources) are listed once, the remaining sources are skipped with a warning in the server log.

  • Only events with stop_time >= now are included in the output.

HTTP headers

The server always sends:

Cache-Control: no-cache, no-store, must-revalidate
Pragma:        no-cache
Expires:       0
Connection:    close

For /xmltv additionally — Content-Encoding: gzip when Accept-Encoding: gzip is present in the request. For /xmltv.gz — Content-Disposition: inline; filename="xmltv.xml.gz".

The client-side caching ban is intentional: XMLTV changes on every EIT import or external XMLTV-source refresh, and the player must not hold stale data indefinitely. An edge cache (nginx) is fully acceptable, however — see the performance section below.

Server cache and how to flush it

The ready XMLTV is cached in the PSS process memory:

  • One entry per channel-set; both body variants (raw and gzip) are stored — repeat requests with Accept-Encoding: gzip or /xmltv.gz do not re-compress the data.

  • Each entry is tagged with an update-time counter. Any EPG update (EIT import, XMLTV source refresh) increments the counter, and the cache is rebuilt on the next request.

Force cache flush:

POST /xmltv/reset-cache

The route is served by the admin server (port 43971/43981), not by epg-server. Empty request body; the response is 200 OK with a JSON envelope.

HTTP response codes

Code

Condition

200 OK

Request processed. Body is an XMLTV document (possibly an empty <tv></tv> on a transient database failure).

401 Unauthorized

Neither Digest nor l/p parameters passed the check (for non-loopback requests).

403 Forbidden

The login exists but the request is not from an allowed IP, or limit-day has expired.

404 Not Found

Any URL other than /xmltv and /xmltv.gz.

405 Method Not Allowed

Method is not GET.

Error body is a fixed-format JSON envelope:

{"status": 401, "message": "Unauthorized"}

Performance and scaling

Server cache

The server cache serves repeated requests to a single channel-set without touching SQLite — by copying the ready body.

Building XMLTV from scratch (cache miss) is more expensive: a separate SELECT on channel_name is issued per channel, and on event_text and event_rating per event. Approximate build times:

Output size

Cache hit

Cache miss (build)

100 channels / day

tens of ms

~0.5–1 s

500 channels / day

~50 ms

2–5 s

1000+ channels / week

~100–300 ms

5–15 s

For most middleware it is acceptable to fetch XMLTV every few hours or once a day.

When an external reverse-proxy (nginx) is needed

Unlike /data/epg/channel (short JSON responses), XMLTV is a single large document per channel-set, ideally suited for an edge cache:

  • Tens to hundreds of clients per channel-set — the internal PSS cache is usually enough if they poll XMLTV every hour-to-day.

  • Thousands of concurrent clients — a caching reverse-proxy is recommended. Serving an XMLTV file to hundreds/thousands of requests is, first of all, a network load (hundreds of KB – a few MB per response) that is best taken off PSS.

  • Geographically distributed delivery — a CDN/edge cache is unavoidable regardless of client count.

PSS sends Cache-Control: no-cache, so nginx must be told explicitly to ignore the upstream header and keep its own TTL.

Sample nginx configuration

# /etc/nginx/conf.d/pss-xmltv.conf

proxy_cache_path /var/cache/nginx/pss-xmltv
                 levels=1:2
                 keys_zone=pss_xmltv:8m
                 max_size=4g
                 inactive=2h
                 use_temp_path=off;

upstream pss_epg {
    server 127.0.0.1:10444;
    keepalive 16;
}

server {
    listen 80;
    # listen 443 ssl http2;     # SSL termination makes sense here
    server_name epg-files.example.com;

    # Do not enable gzip on: /xmltv.gz is already compressed, and /xmltv
    # arrives gzip-encoded from PSS — re-compressing is pointless.

    location ~ ^/xmltv(\.gz)?$ {
        proxy_pass http://pss_epg;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # PSS returns no-cache; force-cache it at the edge.
        proxy_ignore_headers Cache-Control Expires Set-Cookie;
        proxy_hide_header   Cache-Control;
        proxy_hide_header   Pragma;
        proxy_hide_header   Expires;

        # Cache key = full URL including query (login/password in /xmltv?l=...&p=...)
        # produce distinct keys for different accounts with different channel-sets.
        proxy_cache_key     "$scheme$host$request_uri";

        proxy_cache         pss_xmltv;
        proxy_cache_valid   200      30m;       # XMLTV changes infrequently
        proxy_cache_valid   401 403  1m;
        proxy_cache_lock           on;          # coalesce cache misses
        proxy_cache_lock_timeout   60s;         # XMLTV build may take seconds
        proxy_cache_use_stale      error timeout updating
                                   http_500 http_502 http_503;

        # Large buffer: XMLTV for a big channel-set can reach
        # several megabytes.
        proxy_buffering      on;
        proxy_buffers        16 256k;
        proxy_buffer_size    256k;
        proxy_busy_buffers_size 1m;

        # Let the client cache for a short period.
        add_header Cache-Control "public, max-age=600";
        add_header X-Cache-Status $upstream_cache_status always;
    }
}

Notes and recommendations

  • TTL ``proxy_cache_valid 200 30m`` — XMLTV rarely changes more often than every half hour. If source sync runs hourly or less often, this can be raised to 1 hour or more; if freshness after POST /xmltv/reset-cache matters, lower it.

  • ``proxy_cache_lock_timeout 60s`` — increased compared to /data/epg/channel (where 5 seconds is typical), because building the XMLTV for a large channel-set takes longer.

  • A dedicated ``keys_zone`` — even on a large deployment, unique XMLTV keys are few (login count × channel-set); 8 MB is plenty. max_size is sized by XMLTV volume, not by key count.

  • gzip on the nginx side is unnecessary: for /xmltv.gz the response is already compressed, and for /xmltv PSS itself replies gzip-encoded when Accept-Encoding: gzip is present.

  • HTTPS termination on nginx gives better performance under many concurrent TLS handshakes.