Skip to main content
Teamarr is structured in four layers. Each layer has a distinct responsibility and communicates only with the layer below it.
API Layer        → teamarr/api/routes/         (18 route modules, ~134 endpoints)
Consumer Layer   → teamarr/consumers/          (orchestrator, team, event, cache, lifecycle, matching)
Service Layer    → teamarr/services/           (sports_data.py, stream_ordering.py)
Provider Layer   → teamarr/providers/          (ESPN, MLB Stats, HockeyTech, TSDB)

Data flow

HTTP request


 API Layer (FastAPI routes)
     │  validate request, call consumer/service

 Consumer Layer (generation.py)
     │  orchestrate phases, manage state
     ├──► Team Processor   → fetch schedule → resolve template → write XMLTV
     ├──► Event Processor  → fetch streams → match events → create channels
     │         │
     │         ▼
     │    Service Layer (sports_data.py)
     │         │  cache lookup → provider call → cache store
     │         ▼
     │    Provider Layer (ESPN / MLB Stats / HockeyTech / TSDB)

     └──► Dispatcharr Integration (channel CRUD, EPG refresh, reconciliation)


 SQLite Database (WAL mode)
     └──► XMLTV output file

API Layer

The API layer is a FastAPI application (teamarr/api/app.py) serving a REST API at /api/v1/ and a React SPA for all non-API routes.

Route modules

18 modules register approximately 134 total endpoints:
ModuleEndpointsDescription
health.py1Health check and startup state
teams.py8Team CRUD, bulk import, bulk channel ID update
templates.py5Template CRUD
presets.py5Condition preset library
groups.py21Event group CRUD, bulk ops, scheduling
epg.py19Team/event EPG generation, status tracking, preview, stats, cancellation
channels.py11Channel management, reconciliation, reset
dispatcharr.py8M3U accounts, channel/stream profiles, groups
cache.py10Cache refresh, stats, league and team search
stats.py8Generation run stats, processing history, cleanup
sort_priorities.py7Stream ordering rules
aliases.py7Team alias CRUD for stream matching
keywords.py7Exception keywords (pregame, postgame, filler)
detection_keywords.py9Detection keyword CRUD, import/export
subscription.py9Global/per-group subscription config
variables.py3Template variable discovery and introspection
migration.py5V1 to V2 database migration
backup.py11Database backup creation, restore, compression

Startup phases

The lifespan handler in app.py runs through six phases before the application accepts requests:
1

INITIALIZING

Database init and migration detection. If a V1 database is detected, only migration endpoints are served — all others return 503.
2

REFRESHING_CACHE

Team and league cache refresh from all providers. Skippable via SKIP_CACHE_REFRESH=true.
3

LOADING_SETTINGS

Display settings and timezone loaded from the database.
4

CONNECTING_DISPATCHARR

Lazy factory initialization for Dispatcharr integration.
5

STARTING_SCHEDULER

Background EPG cron scheduler started.
6

READY

Fully operational. Health check returns status: healthy.

Generation status state machine

generation_status.py provides a global thread-safe progress tracker. Progress is monotonic — the percentage never decreases.
PhasePercentDescription
starting0%Generation initiated
teams5–50%Processing team EPGs
groups50–95%Processing event groups
saving95–96%Writing XMLTV
complete100%Done

SPA fallback

Non-API routes serve the React frontend: /assets/* serves static JS/CSS files, all other paths return index.html for client-side routing.

Consumer Layer

The consumer layer orchestrates EPG generation, stream matching, channel lifecycle, and Dispatcharr synchronization. It sits between the API routes and the service/provider layers.

Generation workflow

consumers/generation.py provides the single entry point: run_full_generation(). A global lock prevents concurrent runs.
Phase% RangeDescription
M3U Refresh0–5%Refresh Dispatcharr M3U accounts
Teams5–50%Process all active team EPGs
Event Groups50–95%Match streams, create channels, generate EPG
Channel Reassignment93–95%Global channel number rebalancing
Stream Ordering93–95%Apply priority rules to channels
Merge XMLTV95–96%Combine team + group XMLTV output
Dispatcharr Ops96–98%EPG refresh, channel association, cleanup
Reconciliation99–100%Detect/fix channel drift
Shared state: A single SportsDataService instance keeps the event cache warm across all teams and groups. A shared league:date keyed dict prevents duplicate API calls across groups. Cancellation raises GenerationCancelled at phase boundaries.

Event group processor

event_group_processor.py handles the core matching and channel lifecycle pipeline for each event group:
1. Load group config (leagues, team filters, M3U account)
2. Fetch streams from Dispatcharr
3. Filter streams (stale, placeholder, regex include/exclude)
4. Fetch events from providers (parallel, cached)
5. Match streams to events (StreamMatcher)
6. Exclude by timing (past/final/before window)
7. Subscription league filtering (per-group overrides)
8. Create/update channels (ChannelLifecycleService)
9. Generate XMLTV (template resolution)
10. Push to Dispatcharr
11. Track stats

Stream matching

Streams go through two stages — classification then matching. Classifier (matching/classifier.py) categorizes each stream:
CategoryDescriptionExample
TEAM_VS_TEAMContains separator (vs / @ / at)"Cowboys vs Eagles"
EVENT_CARDCombat sports pattern"UFC 315: Main Card"
PLACEHOLDERNo event info"ESPN+ 1", "Coming Soon"
Matcher (matching/matcher.py) tries these methods in priority order:
MethodDescription
cacheFingerprint cache hit from previous generation
exactExact team name match
aliasTeam alias lookup (Detection Library)
fuzzyFuzzy string matching on team names
league_hintDetected league hint narrows search space
The fingerprint cache is keyed by hash(stream_name, group_id, generation). The generation counter increments each EPG run to bust stale entries.

Channel lifecycle

Service (lifecycle/service.py) manages channel creation, sync, and deletion in Dispatcharr using the _safe_update_channel() pattern:
  1. Call Dispatcharr API via manager method
  2. Check OperationResult.success
  3. On success → persist to local DB
  4. On failure → leave DB unchanged → drift re-detected next run (self-healing)
Three parallel context resolution paths must stay in sync:
PathPurposeFile
_create_channelNew channel from matched streamlifecycle/service.py
_sync_channel_settingsUpdate existing channellifecycle/service.py
EPG GeneratorXMLTV channel name/iconevent_epg.py
All three resolve: name, tvg_id, logo, channel group, profiles, stream profile, channel number, and delete timing from the same event + template context. Dynamic resolver (lifecycle/dynamic_resolver.py) handles {sport} and {league} wildcards in channel group and profile names. It looks up display names from the database and auto-creates groups/profiles in Dispatcharr if they don’t exist. Reconciliation (lifecycle/reconciliation.py) detects and fixes inconsistencies between the local DB and Dispatcharr. It runs automatically at the end of each generation.
Issue TypeDescriptionAction
orphan_teamarrDB record but no Dispatcharr channelDelete DB record
orphan_dispatcharrDispatcharr channel but no DB recordLink or ignore
duplicateMultiple channels for same eventMerge or keep first
driftSettings mismatch (name, streams, profiles)Update Dispatcharr

Service Layer

services/sports_data.py orchestrates provider calls with two-tier caching: an in-memory PersistentTTLCache during generation (fast), with a background flush to SQLite every 2 minutes.
MethodTTLDescription
get_events(league, date)8h (30d if all final)All events for a league on a date
get_team_schedule(team_id, league)8hTeam’s upcoming schedule
get_team(team_id, league)24hTeam metadata
get_team_stats(team_id, league)4hRecord, standings
get_single_event(event_id, league)30mLive event with scores
Provider selection uses priority — lower number = tried first:
ProviderPriorityCoverage
ESPN0Primary — most leagues
MLB Stats40MiLB (Triple-A through Rookie)
HockeyTech50CHL, AHL, PWHL, USHL
TSDB100Cricket, Australian sports, rugby, boxing, Scandinavian leagues

Template Engine

The template engine resolves {variable} placeholders in EPG titles, descriptions, and filler content. It is defined in teamarr/templates/.

194 variables

Across 17 categories, registered via decorator in templates/variables/.

20 condition evaluators

Defined in templates/conditions.py. Lower priority = evaluated first.

Suffix rules

.next and .last suffixes for multi-game context (next/last game).

Variable resolution pipeline

  1. Parse {variable} and {variable.suffix} patterns from the template string
  2. Look up each variable in the VariableRegistry
  3. Check the variable’s SuffixRules to determine valid game contexts
  4. Call the variable’s extractor function with the appropriate GameContext
  5. Replace placeholders with resolved values
  6. Clean up artifacts (empty parentheses, double spaces, trailing punctuation)

Suffix rules

SuffixContextExample
{var} (base)Current/next game{game_date}"Mar 15"
{var.next}Next scheduled game{game_date.next}"Mar 18"
{var.last}Last completed game{game_date.last}"Mar 12"
RuleBase.next.lastUsed by
ALLYesYesYesMost variables (opponent, game_date, scores)
BASE_ONLYYesNoNoTeam constants (team_name, league, sport)
BASE_NEXT_ONLYYesYesNoOdds (no odds for past games)

Variable categories

CategoryKey variables
Identityteam_name, opponent, league, sport, team_short, matchup_short, exception_keyword
Combatfighter1, fighter2, card_segment, event_title, event_number, fight_result, weight_class
Conferencecollege_conference, pro_division, pro_conference, pro_conference_abbrev
Recordsteam_record, opponent_record, team_wins, team_losses, team_win_pct
Streakswin_streak, loss_streak, streak, streak_length, streak_type
Home/Awayis_home, is_away, vs_at, home_team, away_team, away_team_short
Scoresteam_score, opponent_score, final_score, score_differential, score_diff
Date & Timegame_date, game_time, days_until, relative_day, today_tonight
Rankingsteam_rank, opponent_rank, is_ranked, is_ranked_matchup, team_rank_display
Oddsodds_spread, odds_over_under, odds_moneyline, has_odds, odds_details
Soccersoccer_match_league, soccer_match_league_id, soccer_primary_league
Statisticsteam_ppg, opponent_ppg, team_papg, opponent_papg
Outcomeresult, result_text, result_lower, overtime_text, winner
Broadcastbroadcast_simple, broadcast_network, broadcast_national_network, is_national_broadcast
Venuevenue, venue_full, venue_city, venue_state
Standingsplayoff_seed, games_back, opponent_playoff_seed
Playoffsis_playoff, is_preseason, is_regular_season, season_type

Conditional descriptions

Templates can define multiple description entries with conditions and priorities. The evaluator selects the first match (lowest priority number). If multiple entries match at the same priority, one is chosen randomly.
[
  {"condition": "win_streak", "condition_value": "5", "priority": 10,
   "template": "{team_name} riding a {win_streak}-game win streak!"},
  {"condition": "is_playoff", "priority": 20,
   "template": "Playoff {sport}: {team_name} vs {opponent}"},
  {"priority": 100,
   "template": "{team_name} vs {opponent}"}
]

Database

Teamarr uses SQLite in WAL mode for all persistent storage. The database file (teamarr.db) is the single source of truth for configuration, teams, templates, event groups, channel state, and run history.
Never delete teamarr.db. It contains all your configuration. Schema upgrades are handled automatically via migrations on startup.

Connection settings

journal_mode = WAL          (Write-Ahead Logging for concurrency)
busy_timeout = 30000        (30 seconds)
foreign_keys = ON           (referential integrity)
check_same_thread = False   (thread-safe access)
row_factory = sqlite3.Row   (dict-like row access)
Current schema version: 71 (stored in settings.schema_version)

Core tables

TablePurpose
settingsSingle-row global configuration (67+ columns)
templatesEPG title/description/filler templates
teamsTeam channel configuration
event_epg_groupsEvent group config (leagues, filters, M3U account, template)
leaguesLeague definitions (provider, sport, display name, logos)
managed_channelsChannels created in Dispatcharr (tvg_id, delete_at, profiles)
detection_keywordsUser-defined stream classification patterns
aliasesTeam name aliases for matching
team_cacheCached team data from providers
service_cacheCached events/teams/stats with TTL
stream_match_cacheFingerprint cache for stream matching
processing_runsEPG generation run statistics

Channel numbering modes

ModeBehavior
strict_blockFixed blocks per league with gaps between. Predictable but wastes numbers.
rational_blockLike strict_block but tightens gaps. More efficient.
strict_compactNo gaps, sequential assignment. Most efficient but numbers shift when channels change.

Dispatcharr integration layer

The teamarr/dispatcharr/ package provides a typed, thread-safe client for Dispatcharr’s REST API.
DispatcharrFactory (singleton)


DispatcharrConnection
  ├── DispatcharrClient  (HTTP + auth + retry)
  ├── ChannelManager     (channel CRUD, O(1) cache)
  ├── EPGManager         (EPG refresh + polling)
  ├── M3UManager         (streams + groups)
  └── LogoManager        (upload + dedup)

HTTP client

SettingValue
Timeout30 seconds
Max retries5
Backoffmin(32s, 1s × 2ⁿ) × jitter(0.5–1.5)
Retryable codes502, 503, 504
Non-retryable401, 403, 404, other 4xx
On 401 responses, the client clears the JWT token and retries once with fresh authentication.

Authentication

TokenManager handles JWT token lifecycle with session isolation by {url}_{username} key. Tokens are proactively refreshed 1 minute before expiry (5-minute token lifetime). Thread-safe with threading.Lock.

OperationResult pattern

All channel CRUD operations return an OperationResult. The lifecycle service enforces a closed-loop contract:
  1. Call Dispatcharr API via manager
  2. Check OperationResult.success
  3. On success → persist to local DB
  4. On failure → leave DB unchanged → drift re-detected next run
This makes Dispatcharr sync self-healing without a retry queue. Profile sync also compares against Dispatcharr’s actual state (not just DB expectations) for additional correctness.

Channel manager cache

The ChannelManager maintains a thread-safe in-memory cache indexed by ID, tvg_id, and channel number for O(1) lookups during generation. The cache is lazily populated on first access and invalidated on mutations. Channel profile semantics:
ValueMeaning
[]No profiles assigned
[0]All profiles (sentinel value)
[1, 2, ...]Specific profile IDs

Database migrations

Teamarr uses a checkpoint + incremental migration system.
Fresh Install          Existing (v2–v42)           Existing (v43+)
     │                       │                           │
     ▼                       ▼                           ▼
 schema.sql           checkpoint_v43.py            Skip checkpoint
(creates v43)       (idempotent → v43)                   │
     │                       │                           │
     └───────────────────────┴───────────────────────────┘


                 v44, v45, ... incremental
                 migrations (connection.py)

Startup order

init_db → verify integrity → structural pre-migrations → reconcile schema → executescript → data migrations → seed cache

Schema reconciliation

reconciliation.py compares the live database against an in-memory reference built from schema.sql. Any missing columns are automatically added on startup. This means adding a new column only requires updating schema.sql — no migration block needed.

Adding a data migration

# In connection.py, inside _run_migrations():
if current_version < 72:
    _add_column_if_not_exists(
        conn, "settings", "my_new_setting", "TEXT DEFAULT 'value'"
    )
    conn.execute("UPDATE settings SET schema_version = 72 WHERE id = 1")
    current_version = 72
Always use idempotent operations (_add_column_if_not_exists, INSERT OR IGNORE, UPDATE ... WHERE col IS NULL) and bump the schema_version DEFAULT in schema.sql.

Build docs developers (and LLMs) love