# M3U/XMLTV Integration

## Overview

ErsatzTV serves M3U playlists and XMLTV guide data via HTTP endpoints. Jellyfin consumes these as an IPTV tuner source.

## Endpoints

| Route | Purpose |
|-------|---------|
| `GET /iptv/channels.m3u` | M3U playlist (channel list + stream URLs) |
| `GET /iptv/xmltv.xml` | XMLTV guide (EPG data) |
| `GET /iptv/logos/{fileName}.jpg` | Uploaded channel logos |
| `GET /iptv/logos/gen?text={name}` | Generated text logos (SkiaSharp, 200x100 PNG) |
| `GET /iptv/channel/{number}.ts` | Transport stream |
| `GET /iptv/channel/{number}.m3u8` | HLS stream |

All defined in `ErsatzTV/Controllers/IptvController.cs` (streams) and `ErsatzTV/Controllers/ArtworkController.cs` (logo generation).

## M3U Generation

**Code**: `ErsatzTV.Core/Iptv/ChannelPlaylist.cs` → `ToM3U()`

The controller captures `Request.Scheme`, `Request.Host`, and `Request.PathBase` from the incoming HTTP request and passes them through MediatR to the playlist builder. All URLs in the M3U output use the request-derived host — they are **not** hardcoded.

Each channel entry includes:
- `tvg-id` — channel number as identifier
- `tvg-chno` — channel number for ordering
- `tvg-name` — channel display name
- `tvg-logo` — logo URL (uploaded artwork or generated text logo)
- `tvc-stream-vcodec` / `tvc-stream-acodec` — codec hints from FFmpeg profile
- `CUID` — base64-encoded UniqueId
- `group-title` — channel group for categorization
- Stream URL (format depends on channel's streaming mode)

The XMLTV guide URL is included as an `#EXTM3U` header attribute.

## XMLTV Generation

**Code**: `ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs`

XMLTV data is pre-generated from Scriban templates (`ErsatzTV/Resources/Templates/_channel.sbntxt`) and cached as XML fragments. At request time, the handler:

1. Reads cached fragments from disk
2. Substitutes `{RequestBase}` with the actual `scheme://host/base` from the request
3. Substitutes `{AccessTokenUri}` with version + optional auth token
4. Filters channels by `ShowInEpg`
5. Combines channel list + program entries into the final XML

## Jellyfin Integration

### Tuner Setup

In Jellyfin, add an IPTV tuner pointing to:
```
http://ersatztv:8409/iptv/channels.m3u
```

Jellyfin reads the M3U to discover channels and their stream URLs. The `Host` header in Jellyfin's request determines what host appears in all embedded URLs — so the hostname used in the tuner URL matters.

### Guide Data

Jellyfin fetches the XMLTV URL embedded in the M3U header for EPG data. This provides program titles, descriptions, and timing for the TV guide.

### Guide Refresh

Jellyfin periodically refreshes the guide data. Channel logos are fetched from the `tvg-logo` URLs in the M3U. If those URLs are unreachable from Jellyfin's network context, logos break.

## Logo URL Issue (Gitea #1)

### The Problem

Channel logos break after Jellyfin guide refreshes. The issue was originally reported as hardcoded `localhost` in `tvg-logo` URLs.

### Investigation Findings

The M3U and XMLTV generation paths **correctly** use request-derived host:
- `ChannelPlaylist.ToM3U()` builds logo URLs from `_scheme`, `_host`, `_baseUrl` (all from the request)
- `GetChannelGuideHandler` substitutes `{RequestBase}` at request time

The **actual** hardcoded `localhost` is in `ChannelLogoGenerator.GenerateChannelLogoUrl()` (`ErsatzTV.Core/Images/ChannelLogoGenerator.cs`, line 82):
```csharp
$"http://localhost:{Settings.StreamingPort}{GetRoute}?{GetRouteQueryParamName}={channel.WebEncodedName}"
```

This method is called only by `WatermarkSelector.cs` (lines 219, 268, 317, 385) — for watermark overlays during transcoding, **not** for M3U/XMLTV output. Since FFmpeg runs on the same host as ErsatzTV, `localhost` is correct for watermarks.

### Possible Explanations

1. The M3U tuner URL in Jellyfin may use a hostname that doesn't resolve correctly from Jellyfin's container (e.g., `localhost` instead of `ersatztv`)
2. There may be a reverse proxy or Docker network issue stripping/rewriting the Host header
3. The issue may actually be about watermarks appearing with broken logos, not M3U `tvg-logo`

### Current Workaround

A script downloads logos from ErsatzTV and base64-uploads them directly to Jellyfin's `/Items/{id}/Images/Primary` API, bypassing the M3U logo URLs entirely. Documented in `server-management` repo (issue #171).
