Architecture Overview¶
System Components¶
┌──────────────────────────────────────────────────────────────┐
│ DAZN Cloud APIs │
│ startup.core.indazn.com · epg.discovery.indazn.com │
│ api.playback.indazn.com · authentication-prod.ar.indazn │
│ drm.playback.indazn.com · image.discovery.indazn.com │
└─────────────┬────────────────────────────┬───────────────────┘
│ HTTPS │ Widevine License
▼ ▼
┌─────────────────────────┐ ┌────────────────────────────────┐
│ PHP Panel (nginx) │ │ playback_request.py │
│ dazn.xctv.stream │ │ request_keys.py (pywidevine) │
│ │ │ curl_cffi Chrome TLS imperson │
│ ┌───────────────────┐ │ └────────────────────────────────┘
│ ┌───────────────────┐ │ │
│ │ function.php │ │ │ decryption keys
│ │ get.php │ │◄─────────────┘
│ │ cron_schedule.php │ │
│ │ discover.php │ │
│ └───────┬───────────┘ │
│ │ │
│ ┌───────▼───────────┐ │
│ │ MariaDB (dazn) │ │
│ │ 8 tables │ │
│ └───────────────────┘ │
└─────────────┬───────────┘
│ xaccel-codec API (start/stop/update)
▼
┌─────────────────────────┐
│ xaccel-codec Server │
│ MPEG-DASH → HLS/TS │
│ DRM decrypt + transcode│
└─────────────────────────┘
Data Flow¶
1. Endpoint Discovery (Startup)¶
Panel → startup.core.indazn.com/v1/main/web
← ServiceDictionary { Rail, Rails, EpgWithDateRange, Playback, SignIn, ... }
→ Probe each version (newest first) to find working URLs
→ Cache to api_endpoints.json (6h TTL)
2. Schedule Build (Nightly Cron — 23:58)¶
cron_schedule.php
→ initApiEndpoints(true) # refresh cached URLs
→ MakeSchedule("yes")
→ DAZN EPG API (epgWithDatesRange)
← Tiles[] with Sport, Competition, AssetId, Start, VerifyAge, etc.
→ Filter by active sports/competitions in DB
→ INSERT into schedule table (one row per event)
→ makeCron() generates /etc/cron.d/cron_dazn entries
(±10 min window around each event start, every 2 min)
→ MakeSchedule() fetches DAZN EPG day-by-day for the full schedule_days range
→ BuildEPG() generates EPG XML covering the full schedule_days range
(per-competition stop_hour duration for live events, 4-hour filler
blocks, per-competition templates from epg_templates table,
deduplicates schedule rows, filename from epg_filename setting)
Template engine: centralized `resolveTemplateVars()` builds 90+
variables across 13 categories (Teamarr-style architecture):
IDENTITY — {team1}, {team2}, {team1_short}, {team1_abbrev},
{team1_pascal}, {team1_normalize}, {league}, {sport}, etc.
HOME_AWAY — {home_team}, {away_team}, {home_team_short},
{home_team_abbrev}, {vs_at}, {vs_@}, etc.
MATCHUP — {matchup}, {matchup_short}, {matchup_abbrev}
DATETIME — {start_time}, {date}, {game_date}, {game_date_short},
{game_date_iso}, {game_day}, {game_time}, {game_time_24h},
{relative_day}, {today_tonight}, {days_until}, etc.
EVENT — {description}, {season}, {asset_id}, {image}, {image_url},
{sport_id}, {competition_id}, {channel_name}
VENUE/SCORES/BROADCAST — populated by TheSportsDB enrichment
(venue, scores, broadcast, badges, jerseys, banners)
RECORDS — {team1_record}, {team2_record} (W-D-L from TSDB standings)
STREAKS — {team1_streak}, {team2_streak} (form-derived from TSDB)
STANDINGS — {team1_position}, {team2_position}, {team1_points},
{team2_points}, {playoff_seed}, {games_back} (TSDB league table)
ODDS — placeholder category (requires external betting API)
PLAYOFFS — {season_type}
`applyTemplate()` replaces placeholders and cleans empty-var artifacts.
TheSportsDB enrichment: `tsdbEnrichEvent()` called at end of
`resolveTemplateVars()` — fuzzy team matching, venue lookup,
event search, league standings, TV schedule. Results cached in
`tsdb_cache` table.
EPG XML enrichment: `<category>` (per-template), `<video>`,
`<audio>`, `<icon>` (poster URL), `<date>` (always),
`<live />` and `<new />` (main events only, always)
→ SendScheduleViaTelegram() (optional)
3. Stream Acquisition (Per-Event Cron)¶
get.php <asset_id> <channel_name>
→ load_token() (get_token or refresh_token)
→ Check schedule status (skip if already streaming)
→ PIN verification (if verify_age=1)
→ DAZN Playback API (dynamic endpoint)
← PlaybackDetails[] with ManifestUrl, LaUrl, CdnName, CdnToken
→ Select CDN from configured list (cdn_takes)
→ Append CdnToken to ManifestUrl (if present)
→ pssh_kid_grabber() fetches MPD, extracts PSSH/KID
→ request_keys.py (curl_cffi) → pywidevine CDM → Widevine license server
← decryption_key (KID:KEY,KID:KEY)
→ Build xaccel-codec URL: ManifestUrl + decryption_key + segment_with_manifest_params=1 + http_persistent=0
→ update() via /dynamic-url API (parses decryption_key from URL)
→ update_decryption_keys() via /decryption-key-update API
→ update_stream_user_agent() sets Chrome UA on xaccel-codec stream config
→ stop() + start() each resolution channel on xaccel-codec
→ update_status() marks event as active
4. Stream Teardown (reset_status.php via cron)¶
reset_status.php
→ SELECT active events past stop_hour (interpreted as minutes)
→ stop() each resolution channel on xaccel-codec
→ UPDATE schedule status = 0
Database Schema¶
| Table | Purpose | Key Columns |
|---|---|---|
settings |
Global config (1 row) | credentials, device IDs, xaccel-codec config, Apprise, epg_filename |
epg_templates |
Named EPG template sets | name, live_title, live_desc, upcoming_title, upcoming_desc, fallback_title, fallback_desc, poster_url, include_date, include_live, include_new, category |
sports |
Sport categories | sport_id (DAZN ID), name, status |
competition |
Leagues/tournaments | competition_id, sport_id (FK), resolutions, channel config, stop_hour (minutes), xaccell_category, channel_logo, video_codec, video_bitrate, video_maxrate, epg_template_id (FK → epg_templates), group_id (FK → competition_groups, nullable) |
competition_groups |
Merged competition groups | name, channel_name, num_ch, stop_hour, channel_logo, epg_template_id |
schedule |
Daily event schedule | asset_id, channel_name, start_time, status, verify_age |
log |
Operation log | channel_name, value, datetime |
proxy |
HTTP proxy servers | name, country, url |
users |
Panel authentication | username (unique), password_hash (bcrypt), totp_secret, created_at |
user_2fa_remember |
2FA trusted-device tokens (7-day) | user_id, token_hash (SHA-256), expires_at |
user_api_keys |
Panel API keys (per-user) | user_id, label, token_hash (SHA-256), token_prefix, last_used_at |
tsdb_cache |
TheSportsDB response cache | cache_key (unique), cache_value (JSON), expires_at (Unix timestamp) |
File Map¶
| File | Role |
|---|---|
function.php |
Core library: auth, EPG (per-competition/group templates), scheduling (group-aware counters), xaccel-codec (AddChannel/UpdateChannel with per-competition encoder settings, group channel registration, and category assignment), logging, discovery, migrations |
get.php |
Per-event stream acquisition (CLI, called by cron) |
index.php |
Dashboard — stats row (events, streams, competitions, sports) + schedule card grid |
config_dazn.php |
Global settings UI + API endpoints panel |
discover.php |
EPG scanner for sport/competition ID discovery |
competition_events.php |
Full event listing for a single competition (linked from Discover) |
sports.php |
Sport selection page — card grid with enable/disable toggles and competition counts |
edit_competitions.php |
Per-sport competition editing — expandable cards with all channel/encoder fields, EPG template dropdown, and group badge; xaccel-codec channel actions |
competition_groups.php |
Competition group CRUD — create, edit, delete groups; assign/unassign competitions |
epg_templates.php |
EPG template CRUD — create, edit, duplicate, delete named template sets |
schedule.php |
Schedule viewer, manual trigger, download |
cron_schedule.php |
Nightly cron entry point |
reset_status.php |
Stream teardown cron |
make_schedule.php |
UI schedule trigger — thin wrapper around MakeSchedule() (multi-day v6 endpoint) |
card.php |
Visual schedule card (standalone black background for external consumers) |
auth.php |
Session authentication, CSRF, rate limiting, 2FA remember-me helpers |
login.php |
Login page with first-run setup |
logout.php |
Session destruction and redirect |
header.php |
Shared layout header with sidebar navigation |
footer.php |
Shared layout footer |
db.inc.php |
Database connection |
tsdb.php |
TheSportsDB integration client — team search with fuzzy matching, venue/event/broadcast lookup, league standings/records/streaks, DB caching |
playback_request.py |
Playback API request via curl_cffi (Chrome TLS impersonation) |
request_keys.py |
Widevine license acquisition via curl_cffi (Python) |
pywidevine/ |
CDM library for DRM decryption |
add_sport.php |
Add sport form |
add_competition.php |
Add competition form |
add_proxy.php |
Add proxy form |
delete_sports.php |
Delete sport (cascades competitions) |
delete_competition.php |
Delete competition |
proxy.php |
Proxy list management |
log.php |
Log viewer |
api.php |
JSON REST API — token-authenticated headless access to status, schedule, EPG, streams, log, and all mutating operations |
JSON REST API (api.php)¶
Bearer-token authentication using user-generated API keys (Profile →
API Keys) or the legacy xaccel-codec token from settings. API key
validation runs inline (no session_start() or migration overhead)
with output buffering to guarantee clean JSON responses.
Read Endpoints (GET)¶
| Action | Parameters | Description |
|---|---|---|
health |
— | System health check, dashboard stats, schedule_days |
status |
— | Active streams, event counts, xaccel-codec stats |
schedule |
?date=YYYY-MM-DD (optional) |
Full schedule listing |
log |
?limit=N (default 100) |
Last N log entries |
streams |
— | xaccel-codec stream stats and categories |
card |
— | Schedule card data grouped by day |
epg |
— | EPG file metadata, channel list, programme count |
competition_groups |
— | Competition groups with member competitions |
Write Endpoints (POST, JSON body)¶
| Action | Body | Description |
|---|---|---|
make_schedule |
— | Rebuild schedule from DAZN EPG |
build_epg |
— | Regenerate EPG XML file |
start_channel |
{asset_id, channel_name} |
Start a channel via get.php |
stop_channel |
{channel_name} |
Stop a channel on xaccel-codec |
reset_status |
{channel_name} |
Reset channel status to inactive |
refresh |
— | Full refresh: endpoints + schedule + EPG + notification |
truncate_schedule |
— | Clear schedule table |
truncate_log |
— | Clear log table |
Example: curl -H "Authorization: Bearer <token>" https://dazn.xctv.stream/api?action=status
For full request/response documentation with field-level details, see Panel API Reference.
EPG Templates & Enrichment¶
The panel generates XMLTV-format EPG files consumed by IPTV players, Plex/Jellyfin DVR, Dispatcharr, Threadfin, and similar middleware. Every aspect of the generated XML is driven by named EPG templates assigned per-competition or per-competition-group.
Template Assignment¶
Each competition (or competition group) has an epg_template_id column
pointing to a row in the epg_templates table. Assign templates in
Edit Competitions (per-competition dropdown) or Competition
Groups (per-group dropdown). If unassigned, built-in defaults apply.
Template Fields¶
| Field | Where Used | Description |
|---|---|---|
name |
UI only | Human label (e.g. "Football", "MMA", "Default") |
live_title |
Main events | Title template for currently-airing events |
live_desc |
Main events | Description template for currently-airing events |
upcoming_title |
Filler blocks | Title for upcoming-event filler slots |
upcoming_desc |
Filler blocks | Description for upcoming-event filler slots |
fallback_title |
Filler blocks | Title when no future event exists on channel |
fallback_desc |
Filler blocks | Description when no future event exists |
category |
All programmes | Sport/genre label (e.g. Soccer, MMA) |
poster_url |
Main events | URL pattern for programme artwork / thumbnail |
include_date |
All programmes | Checkbox — add <date> tag (YYYYMMDD) |
include_live |
Main events | Checkbox — add <live /> tag |
include_new |
Main events | Checkbox — add <new /> tag |
Placeholders¶
All title, description, and category fields support these placeholders (replaced at EPG build time):
| Placeholder | Resolves To | Example |
|---|---|---|
{title} |
Full DAZN event title | Essen vs. Bayern Munich |
{competition} |
Competition channel_name from the DB (for grouped events, the child competition's channel_name) |
UEFA_EUROPA_LEAGUE |
{league} |
DAZN competition name | Uefa Europa League |
{team1} |
First team (split from title on v / vs / vs.) |
Essen |
{team2} |
Second team | Bayern Munich |
{start_time} |
Formatted start time | 7:30 PM |
{date} |
Formatted date | March 23, 2026 |
{description} |
DAZN event description | (varies) |
{season} |
Current season string | 25/26 |
Poster URL additionally supports three normalized placeholders. These strip all non-alphanumeric characters and lowercase the result, making them safe for URL paths:
| Placeholder | Resolves To | Example |
|---|---|---|
{team1_normalize} |
Normalized team 1 | essen |
{team2_normalize} |
Normalized team 2 | bayernmunich |
{league_normalize} |
Normalized DAZN competition name | uefaeuropaleague |
{competition_normalize} |
Normalized DB channel_name | uefaeuropaleague |
EPG XML Tags Generated¶
Every <programme> element includes these tags:
| XML Tag | Applies To | Description |
|---|---|---|
<title> |
All | Rendered from template (live_title, upcoming_title, or fallback_title) |
<desc> |
All | Rendered from template (live_desc, upcoming_desc, or fallback_desc) |
<category> |
All (if set) | From template category field, with placeholders resolved |
<icon src="..."> |
Main events (if set) | From template poster_url, with all placeholders (including normalized) resolved |
<date> |
All (if enabled) | Event date in YYYYMMDD format (per-template toggle) |
<video> |
All | <video><quality>HDTV</quality></video> |
<audio> |
All | <audio><stereo>stereo</stereo></audio> |
<live /> |
Main events (if enabled) | Indicates a live broadcast (per-template toggle, not on fillers) |
<new /> |
Main events (if enabled) | Indicates a new/first-airing programme (per-template toggle, not on fillers) |
Game Thumbs Integration¶
Game Thumbs is a self-hosted
matchup thumbnail service. Set the Program Poster URL to generate
<icon> tags that pull artwork from your Game Thumbs instance.
Poster URL pattern:
Replace game-thumbs:3000 with your actual Game Thumbs host and port.
Image types — Game Thumbs supports three output formats in the URL path suffix:
| Suffix | Resolution | Orientation |
|---|---|---|
/thumb |
1440×1080 | Landscape |
/cover |
1080×1440 | Portrait |
/logo |
Original / 1024×1024 | Square (transparent for matchups) |
League codes — the {league_normalize} placeholder produces a
lowercase alphanumeric string from the competition's channel_name.
Game Thumbs uses its own short league codes. For best results, set
competition channel_name values to match Game Thumbs codes. Common
mappings:
| DAZN Competition | Recommended channel_name |
Game Thumbs Code |
|---|---|---|
| Uefa Europa League | europa |
europa |
| Uefa Europa Conference League | conference |
conference |
| UEFA Champions League | uefa |
uefa |
| English Premier League | epl |
epl |
| La Liga | laliga |
laliga |
| Serie A | seriea |
seriea |
| Bundesliga | bundesliga |
bundesliga |
| Ligue 1 | ligue1 |
ligue1 |
| MLS | mls |
mls |
| NFL | nfl |
nfl |
| UFC | ufc |
ufc |
| NBA | nba |
nba |
See the Game Thumbs Supported Leagues page for the full list of 100+ supported league codes.
Team name matching — Game Thumbs performs fuzzy matching on team
names, so {team1_normalize} values like bayernmunich or
manchesterunited will resolve correctly. City names, abbreviations,
and partial names are also supported.
Example EPG Output¶
A live event with category and poster URL configured:
<programme start="20260323193000 +0000" stop="20260323220000 +0000" channel="EUROPA01">
<title lang="en">LIVE europa Action: Essen vs. Bayern Munich</title>
<desc lang="en">Watch as Essen take on Bayern Munich in live europa action.</desc>
<category lang="en">Soccer</category>
<icon src="http://game-thumbs:3000/europa/essen/bayernmunich/thumb" />
<date>20260323</date>
<video><quality>HDTV</quality></video>
<audio><stereo>stereo</stereo></audio>
<live />
<new />
</programme>
A filler block (no <live />, <new />, or <icon>):
<programme start="20260323150000 +0000" stop="20260323190000 +0000" channel="EUROPA01">
<title lang="en">UPCOMING europa Action: Essen vs. Bayern Munich</title>
<desc lang="en">Upcoming at 7:30 PM Essen vs. Bayern Munich.</desc>
<category lang="en">Soccer</category>
<date>20260323</date>
<video><quality>HDTV</quality></video>
<audio><stereo>stereo</stereo></audio>
</programme>
Quick Setup¶
- Create a template — navigate to EPG Templates → New Template.
- Set titles/descriptions — use placeholders like
{league},{team1},{team2}to generate dynamic text. - Set Category — enter the sport name (e.g.
Soccer). This populates the<category>tag in all programmes using this template. - Set Poster URL — paste your Game Thumbs URL pattern:
http://your-host:3000/{league_normalize}/{team1_normalize}/{team2_normalize}/thumb - Enable enrichment tags — check Include Date Tag, Include Live Tag, and Include New Tag as desired.
- Assign template — in Edit Competitions, select the template from the EPG Template dropdown for each competition or group.
- Rebuild EPG — click Make Schedule or call the API:
POST /api?action=build_epg - Point your player — configure your IPTV player / Dispatcharr /
Threadfin to pull the EPG XML from
https://dazn.xctv.stream/<epg_filename>(default:xctv-dazn-epg.xml).