- Add proper datetime validation in API with 400 error responses
- Fix test timestamp format (was creating invalid +00:00Z format)
- Time filters now properly validate and return errors on invalid input
- All 26 tests passing with correct filtering behavior
- Add pytest-based test suite with 32+ test cases
- Test all features: text search, AND/OR operators, regex, whole words
- Test time filtering: airing_at, start/end time ranges
- Test channel/stream/group filtering
- Test pagination and field selection
- Test edge cases and error handling
- Configurable via environment variables (host, port, credentials)
- Include test runner script and documentation
- Support for CI/CD integration with JUnit XML output
Usage:
./tests/run_tests.sh
pytest tests/test_epg_search_api.py -v
DISPATCHARR_HOST=192.168.1.180 pytest tests/test_epg_search_api.py -v
Three fixes:
- TOCTOU: Replace GET-check-INCR with atomic INCR-first-then-check pattern in both connection managers to prevent concurrent requests exceeding max_streams
- Immediate DECR: Decrement profile counter directly in stream generator exit paths (normal completion, client disconnect, error) instead of deferring to daemon thread cleanup which may never execute
- Rollback: Decrement profile counter on create_connection() failure so the reserved slot is released
Fixes#962
Add cron expression support for M3U and EPG refreshes with interactive
builder modal and quick reference examples. Refactor backup scheduling
to use shared ScheduleInput component for consistency.
- New CronBuilder modal with preset buttons and custom field editors
- Info popover with common cron expression examples
- Shared ScheduleInput component for interval/cron toggle
Fix exception handling in _clean_redis_keys() and stop_channel()
where `except Channel.DoesNotExist` didn't catch ValidationError from invalid UUID format, bypassing the Stream fallback path.
When previewing a stream directly (via stream hash), the cleanup in stream_generator.py tried Channel.objects.get(uuid=stream_hash) which failed with ValidationError since the hash is not a UUID. This
prevented release_stream() from running, permanently leaking the
connection counter. With max_streams=1, a single preview permanently blocked all subsequent streams.
Added Stream.objects.get(stream_hash=...) fallback so the cleanup
path works for both Channel (UUID) and Stream (hash) identifiers.
Also added Stream import to stream_generator.py.
Stream.get_stream() was incorrectly removed as dead code in commit
33f68a98. It is actually called get_stream_object()
in views.py when a stream hash (not channel UUID) is used, such as
during stream preview. The variable is named channel but holds a
Stream instance, which masked the dependency during analysis.
Restored with the INCR-first-then-check pattern applied, fixing the
same TOCTOU vulnerability the original method had.
The local `from apps.channels.models import Channel` inside a
conditional block in _handle_client_disconnect() shadowed the top-level import. When the condition was False, Python's function-level scoping caused line 462's Channel.objects.get() to raise UnboundLocalError. Channel is already imported at module level (line 11), making the local import unnecessary.
Discovered when testing proxy changes
Wrap _clean_redis_keys() call in check_if_channel_exists() with
try/except to prevent exception propagation when Channel/Stream
lookup or release_stream() encounters DB or Redis errors.
All 12 call sites for Channel.release_stream() and Stream.release_stream() now check the boolean return value and log warnings when release fails.
Files updated:
- server.py: 4 call sites (zombie cleanup + _clean_redis_keys)
- channel_service.py: 2 call sites (stop_channel)
- stream_generator.py: 1 call site (last client disconnect)
- views.py: 4 call sites (failed init, redirect, init failure, exception)
- url_utils.py: 1 call site (URL generation error)
Also fixed bare except clause in _clean_redis_keys and uncaught
Stream.DoesNotExist in channel_service.stop_channel.
Based on PR #838 by patchy8736
Stream.release_stream() now returns True on success and False when no
profile info could be found in stream_profile:{stream_id}. This allows
callers to detect and handle failed releases.
Enhanced logging to include stream ID for better traceability.
Based on PR #838 by patchy8736
Channel.release_stream() now returns True on success and False when no
stream/profile info could be found for cleanup. This allows callers to
detect and handle failed releases.
When the primary Redis keys (channel_stream/stream_profile) are already cleaned up by the proxy, the method falls back to the channel metadata hash (ts_proxy:channel:{uuid}:metadata) to recover stream_id and profile_id for proper connection counter cleanup.
Uses str(self.uuid) for metadata key lookup - fixes a bug in PR #838
which used self.id (integer PK), but metadata is keyed by UUID.
Enhanced logging now includes channel UUID for better traceability.
Based on PR #838 by patchy8736
Remove the Stream.get_stream() method which had the same TOCTOU race condition as Channel.get_stream()
All stream acquisition goes through
Channel.get_stream(), making this dead code. Removing it eliminates
a potential source of connection capacity leaks
Based on PR #838 by patchy8736
Add _check_and_reserve_profile_slot() method that uses an INCR-first-then-check pattern to atomically reserve connection slots. This eliminates the TOCTOU race condition where separate GET > check > INCR operations allowed concurrent requests to both pass the capacity check, exceeding max_streams.
For profiles with max_streams=0 (unlimited), INCR is skipped entirely. If over capacity, the increment is rolled back with DECR.
Based on PR #838 by patchy8736, adapted from Lua script to native Redis commands for consistency with project style and easier debugging.
Replace raw Python AF_INET socket checks with pg_isready (PostgreSQL) and socket.create_connection (Redis) in modular deployment mode
The raw socket check may fail in some Docker environments depending on networking and DNS configurations, causing indefinite hangs. Replaced database check with native pg_isready
and memstore checck with AF_UNSPEC in the case IPv6 is enabled.
Fixes#952
- multi_worker_connection_manager.py: Add missing count > 0 guard to multi-worker remove_connection()
This method doesn't appear to be called anywhere but added the guard in case it is put back into service
- multi_worker_connection_manager.py: Error handler at line 1042: removed connection_manager=self from cleanup() call since the DECR was already done at line 1036. Prevents double-DECR.
- connection_manager.py: Added connection_key_exists check before calling remove_connection(). The fallback DECR from session data now only fires when the connection tracking key has already expired. Prevents double-DECR.
- stream_generator.py: _schedule_channel_shutdown_if_needed() already guards itself, the if logic created conflict between release_stream() and stop_channel(), so if the stream was already released the shutdown would not be scheduled