The consumer layer orchestrates EPG generation, stream matching, channel lifecycle, and Dispatcharr synchronization. It sits between the API routes and the service/provider layers.
consumers/generation.py provides the single entry point: run_full_generation(). A global lock prevents concurrent runs.
Phase
% Range
Description
M3U Refresh
0–5%
Refresh Dispatcharr M3U accounts
Teams
5–50%
Process all active team EPGs
Event Groups
50–95%
Match streams, create channels, generate EPG
Channel Reassignment
93–95%
Global channel number rebalancing
Stream Ordering
93–95%
Apply priority rules to channels
Merge XMLTV
95–96%
Combine team + group XMLTV output
Dispatcharr Ops
96–98%
EPG refresh, channel association, cleanup
Reconciliation
99–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.
Service (lifecycle/service.py) manages channel creation, sync, and deletion in Dispatcharr using the _safe_update_channel() pattern:
Call Dispatcharr API via manager method
Check OperationResult.success
On success → persist to local DB
On failure → leave DB unchanged → drift re-detected next run (self-healing)
Three parallel context resolution paths must stay in sync:
Path
Purpose
File
_create_channel
New channel from matched stream
lifecycle/service.py
_sync_channel_settings
Update existing channel
lifecycle/service.py
EPG Generator
XMLTV channel name/icon
event_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.
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.
Method
TTL
Description
get_events(league, date)
8h (30d if all final)
All events for a league on a date
get_team_schedule(team_id, league)
8h
Team’s upcoming schedule
get_team(team_id, league)
24h
Team metadata
get_team_stats(team_id, league)
4h
Record, standings
get_single_event(event_id, league)
30m
Live event with scores
Provider selection uses priority — lower number = tried first:
Provider
Priority
Coverage
ESPN
0
Primary — most leagues
MLB Stats
40
MiLB (Triple-A through Rookie)
HockeyTech
50
CHL, AHL, PWHL, USHL
TSDB
100
Cricket, Australian sports, rugby, boxing, Scandinavian leagues
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}"}]
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.
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.
All channel CRUD operations return an OperationResult. The lifecycle service enforces a closed-loop contract:
Call Dispatcharr API via manager
Check OperationResult.success
On success → persist to local DB
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.
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:
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.
# 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.