---
title: EPG server (XMLTV)
url: https://doc.pstreamer.tv/en/manual/epg_server.html
lang: en
product: Perfect Streamer
version: 1.13.3.445
---

# 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](http://wiki.xmltv.org/index.php/XMLTVFormat) schema:

```xml
<?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:

```json
{"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

```nginx
# /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.

## Related endpoints

- `POST /xmltv/reset-cache` — force-flush the server-side XMLTV cache (on the admin server `43971`/`43981`).
- `POST /data/epg/update?s=<src_id>` — force-refresh an external XMLTV source; after a successful refresh the server-side XMLTV cache is flushed automatically.
- `GET /data/epg/channel?…` — JSON EPG output for one channel for a day; see the separate section.

The complete list and detailed description of the HTTP API are given in `manual/http_data_api.txt`.
