- channel_status.py: fold output_format and output_profile_id into the
existing hmget, reducing per-client Redis calls from 3 to 1
- utils.py: collapse 2x and 3x hget per key in scan_iter loops
(get_user_active_connections) to single hmget calls
- views.py: replace 3x hget on channel metadata in the worker-join path
of stream_ts with a single hmget
Group M3U/XC accounts and profiles that share the same provider login into auto-assigned ServerGroups keyed by credential fingerprint. Enforce combined Redis limits for live TV and VOD via apps/m3u/connection_pool.py.
- Per-profile fingerprinting (XC transforms and STD stream URLs)
- VOD profile selection tries alternates when default credential pool is full (fixes live then VOD failure)
- Stats UI shows provider login from active stream URL
- Tests: apps/m3u/tests/test_connection_pool.py (11 tests, all passing)
Co-authored-by: Cursor <cursoragent@cursor.com>
- Fix NameError in sd_lineups_search: add dispatcharr_version import
- Fix DELETE handler: persist changesRemaining from SD response to custom_properties
- Fix lockout reset time: calculate next midnight UTC (00:00Z) instead of
rolling now+24h, matching SD's documented reset behavior
- Remove redundant explicit index from SDProgramMD5: unique_together already
creates this index in Postgres
- Regenerate 0023_schedules_direct migration to incorporate all changes cleanly
in a single migration
- Fix NameError in sd_lineups_search: add dispatcharr_version import
- Fix DELETE handler: persist changesRemaining from SD response to custom_properties
- Fix lockout reset time: calculate next midnight UTC (00:00Z) instead of
rolling now+24h, matching SD's documented reset behavior
- Remove redundant explicit index from SDProgramMD5: unique_together already
creates this index in Postgres
- Rename api_key to password (RemoveField + AddField) for clarity and reusability
- Move sd_changes_remaining/sd_changes_reset_at to custom_properties JSONField
instead of nullable columns on all EPGSource rows
- Replace sd_program_md5 column on ProgramData with SDProgramMD5 relation table
keyed by epg_source + program_id, fixing the key mismatch bug in program
MD5 delta detection
- Fix total_changed counter: .items() -> .values() so len() returns date count
not tuple length
- Add missing SD lineup management endpoints to api_views.py
(sd_lineups GET/POST/DELETE, sd_lineups_search POST)
- Regenerate migration 0023_schedules_direct using Django to ensure correct
index names and schema
- Add EPGSource username field and SDScheduleMD5 model for MD5 delta sync
- Implement full Schedules Direct fetch with SHA1 auth, token caching,
20-day schedule window, and MD5-based delta sync to minimize API usage
- Add lineup manager UI allowing users to search and manage SD lineups
- Add stations-only fetch on source creation for immediate EPG matching
- Enforce 2-hour minimum refresh interval to respect SD rate limits,
with first-fetch bypass when no prior full refresh exists
- Add real-time progress updates via synchronous Redis WebSocket delivery,
bypassing gevent.spawn for reliable delivery from Celery prefork workers
- Refactor EPGsTable status cell into standalone EPGStatusCell component
with direct Zustand subscription for live progress bar updates
- Guard XMLTV parse trigger for channels linked to SD EPG sources
- Add 15 backend tests covering auth, credentials, signals, and serializers
Migrations: 0023 (username field), 0024 (api_key help_text), 0025 (SD fields + SDScheduleMD5)
gevent registers a pthread_atfork handler that never yields. Any
subprocess.Popen/run call in a uWSGI greenlet hangs indefinitely at
fork() before the child process even starts.
- dispatcharr/gevent_patch.py: new; monkey.patch_all() + psycogreen,
loaded via uWSGI import= in all four ini files
- live_proxy/input/manager.py: posix_spawn + _SpawnedProcess; removed
dead forwarder code
- live_proxy/input/http_streamer.py: O_NONBLOCK on relay pipe + EAGAIN
retry (blocking write to a full pipe stalls the gevent hub)
- live_proxy/utils.py: new posix_spawn_proc() helper with O_NONBLOCK
stdin, shared by both output managers
- live_proxy/output/fmp4/manager.py, output/profile/manager.py:
posix_spawn_proc(); _write_all() treats EAGAIN (None) as cooperative
select.select wait instead of fatal error
- core/views.py (stream_view): posix_spawn; fixed pre-existing bug
where return StreamingHttpResponse(...) was indented inside
stream_generator, making the success path always return None
- connect/handlers/script.py: _posix_run() with posix_spawn +
cooperative select reads + non-blocking waitpid; fixes deadlock when
a script integration fires on a uWSGI-context event (client_connect
fires in the live proxy, not Celery)
- Enable gevent cooperative multitasking in all uWSGI worker configs
(gevent-early-monkey-patch + import dispatcharr.gevent_patch)
- Rewrite WebSocket group sends to bypass asyncio in gevent workers:
_gevent_ws_send() replicates the channels_redis 4.x wire format
directly via synchronous Redis so send_websocket_update() and
_send_async() no longer fail silently after epoll is patched out
- Fix PostgreSQL connection exhaustion: CONN_MAX_AGE=0 + explicit
close_old_connections() in stream manager and cleanup watchdog loops
- Fix stream proxy race: register client before the connect-wait loop
so the cleanup watchdog never sees zero clients on a live channel
- Channel list/logo/profile queryset optimisations: conditional DISTINCT,
EXISTS semi-joins for filter-options, channel_count annotation to
eliminate N+1 in LogoSerializer, prefetched memberships in
ChannelProfileSerializer
- JsonResponse for channel ID list and summary endpoints
Plugins live in /data/plugins/<slug>/plugin.py, outside INSTALLED_APPS,
so celery's autodiscover_tasks() never imports them. Any plugin using
module-level @shared_task for cron-scheduled work therefore has its
task unregistered with the worker after every worker restart, until
something else (a connect/utils.trigger_event call, etc.) lazily
imports the plugin module via PluginManager.discover_plugins.
In the meantime, beat fires the periodic task on time and the worker
rejects it:
ERROR celery.worker.consumer.consumer Received unregistered task of
type 'telegram_alerts.send_daily_report'.
PeriodicTask.last_run_at advances anyway, so the failure is silent at
the default INFO log level.
Fix: hook celery's worker_ready signal in dispatcharr/celery.py to
call PluginManager.discover_plugins(sync_db=False) on every worker
boot. Importing the plugin modules runs their @shared_task decorators,
registering the tasks before beat starts firing.
Exception handling is intentionally broad: if one plugin's plugin.py
has an import error, the worker must still come up — discovery failure
should not be a fatal error for the whole worker process.
Tests in tests/test_celery_plugin_discovery.py verify:
- the handler calls discover_plugins(sync_db=False)
- the handler swallows PluginManager.get() exceptions
- the handler swallows discover_plugins() exceptions
- the handler is actually connected to the worker_ready signal
Confirmed the test fails against current dev with the exact
'cannot import name discover_plugins_on_worker_ready' that motivates
this change, and passes with the fix.