Tag / Commit
latest
Dispatcharr version
0.31.0
Browser
No response
Affected area
Generation / EPG output
What happened?
Using Claude to review how the code works it determined that Teamarr if forcing action before Dispatcher has written what it requires to be accurate.
Creating this bug report to have dev review for accuracy of code operation and suggested fix.
Expected behavior
I'll pull down the repo and look at the Dispatcharr sync and Channels DVR refresh paths.Let me check Dispatcharr's own behavior for these endpoints, since the race depends on it. I found a concrete cause, and it's a race between Teamarr, Dispatcharr's async program parsing, and the Channels DVR trigger. It explains exactly why the channel appears but the guide is empty, and why a later manual refresh in Channels fixes it.
What the code does
In teamarr/consumers/generation.py, the order inside a run is:
- Write the merged XMLTV (line ~450)
- Refresh Dispatcharr's EPG source and wait for it —
epg_manager.wait_for_refresh(...) (line 482)
- Then associate EPG data with channels —
associate_epg_with_channels(...) (line 494)
- Deletions, reconciliation, stream audit, cleanup
- Fire the Channels DVR M3U + lineup refresh in a background thread —
_start_media_server_refresh(...) (line 577)
Step 2 happening before step 3 is the problem, because of how Dispatcharr behaves.
Why that breaks new channels
Dispatcharr's EPG import deliberately only parses programme data for EPG entries that are already mapped to a channel. From apps/epg/tasks.py:2167:
Only process EPG entries that are actually mapped to channels… skips programmes for unmapped channels entirely during parsing
create_channel in teamarr/dispatcharr/managers/channels.py:273 never sets epg_data, so a brand-new Teamarr channel is unmapped when the import at step 2 runs. Dispatcharr creates the EPGData row for teamarr-event-<id> but stores zero programmes for it.
Then step 3 calls POST /api/channels/channels/<id>/set-epg/ per channel. On the Dispatcharr side that only queues the work — apps/channels/signals.py:183 fires parse_programs_for_tvg_id as a Celery task, and set_epg itself returns immediately with "EPG refresh queued". Teamarr doesn't wait for any of it (syncer.py:606 fires and forgets).
Worse, two things stretch that queue out:
- Every managed channel gets a redundant
set-epg call every run. associate_epg_with_channels doesn't compare against the channel's current epg_data_id, and Dispatcharr's Channel post_save signal doesn't check whether the value actually changed either (unlike the ChannelOverride handler right below it, which does). So each run queues one full-XMLTV-file parse task per channel, and the parse your new channel needs sits behind all of them.
- Those tasks defer if the source refresh lock is still held, 15 seconds at a time (
_EPG_REFRESH_DEFER_SECONDS = 15, tasks.py:70 and tasks.py:1668).
So when step 5 fires, Channels DVR pulls /output/epg from Dispatcharr and gets your channel with no <programme> elements. Teamarr's log verification still passes, because "[DVR] Fetched guide data" and "Indexed N airings" refer to the whole lineup, not your specific channel. Channels then keeps that empty guide until its next scheduled or manual refresh — which is why manually refreshing works.
It also explains why a second generation run fixes it: by then the channel is mapped, so the import at step 2 includes it.
How to confirm in about a minute
Right after a Teamarr run that creates a channel, in Dispatcharr's logs look for the relative timing of:
Refreshing program data for tvg_id: teamarr-event-...
Deferring parse_programs_for_tvg_id(...) for 15s: source refresh in progress
and compare against Teamarr's [CHANNELSDVR] Triggered EPG refresh for lineup '...'. If the Dispatcharr line lands after the Teamarr line, that's it. You can also curl Dispatcharr's /output/epg immediately after a run and grep for your tvg-id — you'll see the <channel> element with no programmes.
Fixes worth raising upstream
The minimal fix is to re-import after association, before the media-server trigger: do associate_epg_with_channels first, and only then call wait_for_refresh (or call it a second time when result.epg_association["associated"] included any new mappings). That way the import itself sees the mapping and parses programmes synchronously, and the existing wait_for_refresh gating already blocks until Dispatcharr reports success.
Two supporting changes:
- Switch
associate_epg_with_channels to Dispatcharr's POST /api/channels/channels/batch-set-epg/ (api_views.py:2477). It skips unchanged associations and only dispatches parses for EPG ids that actually changed, and it returns channels_updated / programs_refreshed — so Teamarr gets a clean signal for whether it needs to wait at all. Right now Teamarr is queuing dozens of redundant whole-file parses per run.
- Before firing the Channels DVR refresh, verify programme data exists for the newly associated
tvg_ids. Teamarr already has the tool for this: EPGManager.search_programs(tvg_id=...). Polling that for the new ids would make the CDVR trigger evidence-based on the Dispatcharr side, the same way it already is on the CDVR side.
As a stopgap until it's patched, the reliable workaround is to have something refresh the Channels lineup a couple of minutes after the Teamarr run finishes — a cron hitting PUT /dvr/lineups/XMLTV-<source> on your DVR — or to run generation on a tighter schedule so the follow-up run repairs the guide sooner.
Steps to reproduce
Run a generation and check Channels DVR Guide.
Logs
Screenshots
No response
Tag / Commit
latest
Dispatcharr version
0.31.0
Browser
No response
Affected area
Generation / EPG output
What happened?
Using Claude to review how the code works it determined that Teamarr if forcing action before Dispatcher has written what it requires to be accurate.
Creating this bug report to have dev review for accuracy of code operation and suggested fix.
Expected behavior
I'll pull down the repo and look at the Dispatcharr sync and Channels DVR refresh paths.Let me check Dispatcharr's own behavior for these endpoints, since the race depends on it. I found a concrete cause, and it's a race between Teamarr, Dispatcharr's async program parsing, and the Channels DVR trigger. It explains exactly why the channel appears but the guide is empty, and why a later manual refresh in Channels fixes it.
What the code does
In
teamarr/consumers/generation.py, the order inside a run is:epg_manager.wait_for_refresh(...)(line 482)associate_epg_with_channels(...)(line 494)_start_media_server_refresh(...)(line 577)Step 2 happening before step 3 is the problem, because of how Dispatcharr behaves.
Why that breaks new channels
Dispatcharr's EPG import deliberately only parses programme data for EPG entries that are already mapped to a channel. From
apps/epg/tasks.py:2167:create_channelinteamarr/dispatcharr/managers/channels.py:273never setsepg_data, so a brand-new Teamarr channel is unmapped when the import at step 2 runs. Dispatcharr creates theEPGDatarow forteamarr-event-<id>but stores zero programmes for it.Then step 3 calls
POST /api/channels/channels/<id>/set-epg/per channel. On the Dispatcharr side that only queues the work —apps/channels/signals.py:183firesparse_programs_for_tvg_idas a Celery task, andset_epgitself returns immediately with "EPG refresh queued". Teamarr doesn't wait for any of it (syncer.py:606fires and forgets).Worse, two things stretch that queue out:
set-epgcall every run.associate_epg_with_channelsdoesn't compare against the channel's currentepg_data_id, and Dispatcharr'sChannelpost_save signal doesn't check whether the value actually changed either (unlike theChannelOverridehandler right below it, which does). So each run queues one full-XMLTV-file parse task per channel, and the parse your new channel needs sits behind all of them._EPG_REFRESH_DEFER_SECONDS = 15,tasks.py:70andtasks.py:1668).So when step 5 fires, Channels DVR pulls
/output/epgfrom Dispatcharr and gets your channel with no<programme>elements. Teamarr's log verification still passes, because "[DVR] Fetched guide data" and "Indexed N airings" refer to the whole lineup, not your specific channel. Channels then keeps that empty guide until its next scheduled or manual refresh — which is why manually refreshing works.It also explains why a second generation run fixes it: by then the channel is mapped, so the import at step 2 includes it.
How to confirm in about a minute
Right after a Teamarr run that creates a channel, in Dispatcharr's logs look for the relative timing of:
Refreshing program data for tvg_id: teamarr-event-...Deferring parse_programs_for_tvg_id(...) for 15s: source refresh in progressand compare against Teamarr's
[CHANNELSDVR] Triggered EPG refresh for lineup '...'. If the Dispatcharr line lands after the Teamarr line, that's it. You can also curl Dispatcharr's/output/epgimmediately after a run and grep for yourtvg-id— you'll see the<channel>element with no programmes.Fixes worth raising upstream
The minimal fix is to re-import after association, before the media-server trigger: do
associate_epg_with_channelsfirst, and only then callwait_for_refresh(or call it a second time whenresult.epg_association["associated"]included any new mappings). That way the import itself sees the mapping and parses programmes synchronously, and the existingwait_for_refreshgating already blocks until Dispatcharr reports success.Two supporting changes:
associate_epg_with_channelsto Dispatcharr'sPOST /api/channels/channels/batch-set-epg/(api_views.py:2477). It skips unchanged associations and only dispatches parses for EPG ids that actually changed, and it returnschannels_updated/programs_refreshed— so Teamarr gets a clean signal for whether it needs to wait at all. Right now Teamarr is queuing dozens of redundant whole-file parses per run.tvg_ids. Teamarr already has the tool for this:EPGManager.search_programs(tvg_id=...). Polling that for the new ids would make the CDVR trigger evidence-based on the Dispatcharr side, the same way it already is on the CDVR side.As a stopgap until it's patched, the reliable workaround is to have something refresh the Channels lineup a couple of minutes after the Teamarr run finishes — a cron hitting
PUT /dvr/lineups/XMLTV-<source>on your DVR — or to run generation on a tighter schedule so the follow-up run repairs the guide sooner.Steps to reproduce
Run a generation and check Channels DVR Guide.
Logs
Screenshots
No response