Commit graph

358 commits

Author SHA1 Message Date
SergeantPanda
29739ede36 Enhancement: VOD start/stop notifications and system events.
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run
2026-04-23 18:03:02 -05:00
SergeantPanda
eb9dd391d5 Enhancement: Consolidate client connection notifications and update timestamp handling; replace 'connected_since' with 'connected_at' for improved accuracy in duration calculations. 2026-04-23 11:03:02 -05:00
SergeantPanda
29228612aa perf/fix: eliminate per-tick DB queries in stats; fix channel notifications
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run
- Write channel_name (and stream_name fallback) into Redis metadata at
  channel init time; read directly from Redis in get_basic_channel_info,
  removing Stream and M3UAccountProfile DB queries on every stats tick
- Pass channel.name from views.py into ChannelService.initialize_channel
  via new optional channel_name param, skipping the redundant Channel DB
  lookup on the normal streaming path
- Pass stream_name from get_stream_info_for_switch through change_stream_url
  into _update_channel_metadata, skipping the Stream DB lookup on switches
- Resolve m3u_profile_name on the frontend from the playlists store instead
  of querying the DB per tick; StreamConnectionCard builds an id→profile map
- Fix channel start notification showing no name and stop notification
  showing UUID: both now use channel_name from the stats WebSocket payload
2026-04-21 18:55:38 -05:00
SergeantPanda
625dab0357 Enhancement: The default profile in the M3U profiles editor now exposes Search Pattern and Replace Pattern fields
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run
2026-04-21 17:20:45 -05:00
SergeantPanda
d676903825 Enhancement: Cache channel names to reduce repeated DB queries in StreamManager and StreamGenerator. This improves performance by avoiding unnecessary database calls during retries and logging events. (Fixes #1138)
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
2026-04-17 10:55:41 -05:00
SergeantPanda
6cd6d3ef9a Enhancement: - **Output bitrate DB persistence**: the ffmpeg_output_bitrate stat is no longer written to the database on every FFmpeg stats tick (~2/second). Instead, a local exponential moving average (EMA, α=0.1) accumulates readings continuously. The first 10 samples (~5 seconds) are discarded as warmup to avoid polluting the average with FFmpeg's unstable ramp-up values. After warmup, the smoothed value is flushed to the database at most once every 30 seconds, and a final flush occurs when the stream stops but only if the EMA has been seeded (i.e. the stream ran past warmup). Streams that stop during warmup leave the existing database value untouched, preserving previously accurate measurements when channel-hopping. 2026-04-16 17:15:54 -05:00
SergeantPanda
f98bb183da Bug Fix: Fixed a provider TCP connection leak in the VOD proxy 2026-04-13 19:33:22 -05:00
SergeantPanda
55048b60e3 fix(vod-proxy): prevent double profile decrement and clean up stream counter naming 2026-04-13 18:40:33 -05:00
SergeantPanda
d73071b20f Merge branch 'dev' of https://github.com/Dispatcharr/Dispatcharr into pr/firestaerter3/1125 2026-04-13 17:42:43 -05:00
SergeantPanda
b629836b3d Bug Fix: Fixed stream switch metadata (url, user_agent, stream_id, m3u_profile) being written to Redis before the switch was confirmed to succeed 2026-04-12 12:02:20 -05:00
SergeantPanda
65d14644b4 Bug Fix: Fixed the next_stream rotation endpoint applying the same class of bug 2026-04-12 11:36:08 -05:00
SergeantPanda
ef030b3da0 Bug Fix: Manual stream selection from the Stats page not enforcing M3U profile connection limits in multi-worker deployments 2026-04-12 10:03:46 -05:00
SergeantPanda
c3fb9c0073 security: Fixed missing network_access_allowed checks in the VOD proxy. stream_vod, head_vod, stream_xc_movie, and stream_xc_episode were not checking the STREAMS network policy, unlike the equivalent TS proxy endpoints. 2026-04-10 11:09:27 -05:00
SergeantPanda
fa9a7868ff security: proxy streaming endpoints (stream_ts, stream_xc, stream_vod, head_vod, stream_xc_movie, stream_xc_episode) use @permission_classes([AllowAny]) (access is controlled by the per-stream-type network allow-list inside the view body); the UserAgentViewSet, StreamProfileViewSet, CoreSettingsViewSet, and ProxySettingsViewSet gained get_permissions() methods mapping read actions to IsStandardUser and write actions to IsAdmin; and AuthViewSet.logout was updated to return [Authenticated()]. 2026-04-10 10:47:50 -05:00
SergeantPanda
dcefc6541c chore(cleanup): - Removed dead VODConnectionManager class (apps/proxy/vod_proxy/connection_manager.py) and its associated helpers, which had been superseded by MultiWorkerVODConnectionManager. All active code already used the multi-worker implementation. Removed the unused VODConnectionManager import from vod_proxy/views.py, the unscheduled cleanup_vod_connections task from apps/proxy/tasks.py, and the unscheduled cleanup_vod_persistent_connections task from core/tasks.py.
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run
- Removed dead VOD URL routes: `VODPlaylistView` (playlist generation), `VODPositionView` (position tracking), and the class-based `VODStatsView` (replaced by the existing function-based `vod_stats` view).
- Removed dead `updateVODPosition()` API method from `frontend/src/api.js`, which called the now-removed position tracking endpoint.
2026-04-09 19:59:50 -05:00
SergeantPanda
7f64642003 security: Hardened the HLS proxy change_stream endpoint by converting it from a plain Django view to a DRF @api_view with @permission_classes([IsAdmin]), ensuring the endpoint actually enforces admin-only access. The previous decorator arrangement (@csrf_exempt + @permission_classes) had no effect on a plain Django view. 2026-04-09 19:58:40 -05:00
None
763f42cfff fix: pass TLS parameters to all external connection points in modular mode
- Add setup_pg_ssl_env() to export libpq PGSSL* env vars; all child psql/pg_dump/pg_isready commands inherit TLS automatically

- Move TLS client key permission fix before external PG connections (was running after, defeating the fix)

- Enhance key fix to trigger on ownership mismatch (root-owned 0600 key unreadable by app user)

- Extract key fix to shared script (docker/init/00-fix-pg-ssl-key.sh) sourced by both entrypoints

- Default POSTGRES_PASSWORD to empty in modular mode (cert-only auth sends no spurious password)

- Propagate TLS OPTIONS to backup pg_dump/pg_restore subprocess calls via _get_pg_env()

- Pass SSL kwargs to dropdb management command's psycopg2.connect()

- Add REDIS_SSL_PARAMS to VOD proxy Redis connections missed in original TLS PR

- Add 8-scenario TLS integration test suite (PG mTLS, Redis TLS, verify-full, key fix, Celery, regression)

- Add 6 unit tests for _get_pg_env() and dropdb TLS parameter passing
2026-03-30 20:19:27 -05:00
SergeantPanda
086cc74959 Bug Fix: M3U profile URL rewriting now uses the regex module instead of re across all URL transform code paths (url_utils.transform_url, core/views.py, vod_proxy/_transform_url, tasks.get_transformed_credentials, and the WebSocket live-preview handler in consumers.py). The regex module natively accepts JavaScript/PCRE-style named capture groups ((?<name>...)) without any conversion, eliminating the root cause of patterns that matched in the frontend live preview but failed on the backend with a re.error. As a further improvement, regex also supports variable-length lookbehind assertions (e.g. (?<=a+)), which re rejects with an error; patterns using these will now work correctly on the backend as well. Replace-pattern JS tokens are still normalised before calling regex.sub: $<name>\g<name> and $1/$2/… → \1/\2/… (Python replacement syntax). Also fixed a bug in the WebSocket preview handler where a pattern error was incorrectly returning the search pattern string as the preview output instead of the original URL. (Fixes #1005)
Some checks failed
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Has been cancelled
2026-03-29 17:55:08 -05:00
SergeantPanda
6d06422ddd Enhancement: client_connect and client_disconnect system events now include the **username** of the connected user. The username is stored alongside the client metadata in Redis and included in the event payload for log_system_event calls (making it available to webhook and script integrations). 2026-03-29 16:06:14 -05:00
SergeantPanda
f00d1fe63c Bug Fix/Enhancement:
- Web UI stream preview (`FloatingVideo`) was calling `mpegts.createPlayer()` with all `Config` options (e.g. `enableWorker`, `liveSync`, `headers`) merged into the first `MediaDataSource` argument. mpegts.js only reads `Config` from the optional second argument; unrecognised fields in the first are silently ignored. As a result all player configuration was effectively the library defaults — worker offloading was disabled, latency management had no effect, and the `Authorization: Bearer` header (required for user identification) was never sent. Fixed by splitting into the correct two-argument call. Both `liveBufferLatencyChasing` and `liveSync` have been disabled, eliminating playback-rate fluctuations that caused audible stuttering on live streams. SourceBuffer cleanup thresholds were also relaxed from 10s/5s to 120s/60s to prevent frequent SourceBuffer pauses.
- Web UI stream preview now sends an `Authorization: Bearer` header with each mpegts.js request, identifying the logged-in user. Live channel previews initiated from the web UI now appear on the Stats page with the correct username rather than as unknown user.
2026-03-28 18:04:11 -05:00
SergeantPanda
38f5156ca6 Enhancement: Show username in VOD cards. 2026-03-28 16:46:06 -05:00
SergeantPanda
5f4e066147 Enhancements:
- Connection cards on the Stats page now show the **username** of the connected user in a new User column (between IP Address and Connected). The username is resolved from the user store using the `user_id` stored in Redis client metadata; unauthenticated connections display "Anonymous". (Closes #766)
- `ip_address` and `user_id` were not included in the client info returned by `get_detailed_channel_info()` despite being available in the Redis hash. Both fields are now extracted and returned. (Closes #586)
2026-03-28 13:33:29 -05:00
SergeantPanda
7964b2c2cb fix: enhance stream termination logic to handle multiple connections on the same channel 2026-03-26 20:33:03 -05:00
SergeantPanda
46d4063524 fix: change logging level from info to debug in user connection checks 2026-03-26 20:10:03 -05:00
SergeantPanda
aa6fb033d3 fix: update user stream limit checks to include media_id and rename user_limits_settings key 2026-03-26 20:02:54 -05:00
SergeantPanda
58befaa52b fix: correct indentation in check_user_stream_limits function 2026-03-26 19:14:46 -05:00
SergeantPanda
b671a72707 fix: resolve decode_responses migration bugs and user-limit regressions
Fix a wave of bugs introduced by the user-priority branch's migration
from manual .decode('utf-8') calls to decode_responses=True on the
metadata Redis client:

core/utils.py
- Rewrite _init_client: fix SyntaxError at line 138, missing
  redis_password/redis_user params, cls._client corruption when
  decode_responses=False was requested, and dead retry backoff logic

apps/proxy/utils.py
- Fix get_user_limit_settings() → get_user_limits_settings() (2 sites)
- Fix VOD scan key vod_persistent_connections:* → vod_persistent_connection:*
- Fix undefined channel_id in VOD loop (use content_uuid from Redis hash)
- Remove leftover print(active_connections) debug statement
- Refactor manual cursor SCAN while-loops to scan_iter()

apps/channels/tasks.py
- Fix closure bug in _d(): md.get(key) → md.get(bkey) (was reading
  the outer loop variable instead of the local bytes key)

apps/proxy/ts_proxy/server.py
- Replace stale b'init_time', b'total_bytes', b'state' byte-string
  key lookups and .decode() calls with plain string equivalents

apps/proxy/ts_proxy/services/channel_service.py
- Fix ChannelMetadataField.STATE.encode() / .OWNER.encode() →
  .STATE / .OWNER (pre-existing, broke under decoded client)

apps/proxy/ts_proxy/views.py
- Fix ChannelMetadataField.OWNER.encode("utf-8") → .OWNER

apps/proxy/ts_proxy/client_manager.py
- Fix cid.decode('utf-8') in remove_ghost_clients(): smembers()
  already returns str with decode_responses=True
2026-03-26 18:41:42 -05:00
dekzter
90a600b48f merged in dev 2026-03-26 12:17:43 -04:00
dekzter
a19be96ced fleshed out user limits and termination logic 2026-03-25 17:33:26 -04:00
None
b30a24e2fb feat: add TLS connection support for Redis and PostgreSQL (#950)
- Add 10 env vars for Redis/PostgreSQL TLS (REDIS_SSL, REDIS_SSL_VERIFY, REDIS_SSL_CA_CERT, REDIS_SSL_CERT, REDIS_SSL_KEY, POSTGRES_SSL, POSTGRES_SSL_MODE, POSTGRES_SSL_CA_CERT, POSTGRES_SSL_CERT, POSTGRES_SSL_KEY)
- Propagate SSL params to all Redis connection points: RedisClient, stream view, PubSub, client manager, Celery broker/result backend, Django Channels, and pre-Django startup scripts
- Add Celery SSL config via URL query string params (required by Kombu's internal URL parsing)
- Add PostgreSQL sslmode and certificate options to DATABASES config
- Add startup validation: cert file existence, URL scheme conflict detection, TLS status logging
- Add TLS error hints to Redis connection failure messages
- Add Connection Security panel in System Settings (modular mode only) showing encryption, verification, and mTLS status
- Add PG client key permission fix in web and celery entrypoints for Docker Desktop compatibility (0777 to 0600)
- Add TLS env var passthrough in entrypoint for su - login shells
- Document TLS env vars in modular docker-compose.yml
- Change env_mode from dev/prod to actual DISPATCHARR_ENV value for modular mode detection
2026-03-21 17:24:09 -05:00
firestaerter3
5ad4824c4b fix(vod-proxy): eliminate profile_connections counter stuck-at-nonzero races
Three race conditions in multi_worker_connection_manager could leave
the Redis profile_connections:<id> counter permanently elevated with no
active streams, causing all VOD requests to 503 "All profiles at capacity".

Bug 1 — decrement_active_streams() return value was ignored
All three generator exit paths (normal, GeneratorExit, Exception) called
decrement_active_streams() and unconditionally set decremented = True
regardless of whether the lock was acquired. On lock contention the decrement
was silently skipped, active_streams remained > 0, the subsequent
has_active_streams() check returned True, and _decrement_profile_connections()
was never called. The counter was then stuck until manual DEL.

Fix: add decrement_active_streams_and_check() which performs the decrement and
the "are there remaining streams?" check atomically under a single lock,
eliminating the race window. All three exit paths and the finally block now
use this method and propagate its success/remaining return values.

Bug 2 — non-atomic GET-then-DECR in _decrement_profile_connections()
The previous implementation read the counter with GET then conditionally
called DECR. Two concurrent decrements could both pass the > 0 guard and
both fire, driving the counter to -1. A subsequent _check_and_reserve_profile_slot()
INCR would then produce 0 which passes the <= max_streams check, allowing
an extra stream to bypass the limit on the next request.

Fix: replace GET-then-DECR with a direct DECR (matching the INCR-first
pattern already used by _check_and_reserve_profile_slot) and clamp the
result to 0 if it goes negative.

Bug 3 — has_active_streams() read state without holding the lock
The separate has_active_streams() call after decrement_active_streams()
released its lock left a window where another concurrent stream could
increment active_streams back to 1, causing the profile decrement to be
skipped. This is resolved as a consequence of Bug 1's fix: the new
decrement_active_streams_and_check() method reads active_streams while
the lock is still held, eliminating the window entirely.

Adds tests covering all three scenarios in
apps/proxy/vod_proxy/tests/test_profile_connections.py.

Fixes #1121.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 23:23:36 +01:00
SergeantPanda
d4e52f70db Merge branch 'dev' into pr/cmcpherson274/1105 2026-03-15 21:02:52 -05:00
SergeantPanda
92f98a24c7 fix: increase ghost client multiplier to improve client detection timing 2026-03-15 20:45:55 -05:00
None
55f0a49d68 fix: improve test coverage and add missing metadata constants
- Rewrite ghost client tests to call real ChannelStatus and ClientManager code paths instead of reimplementing detection logic in test helpers
- Rewrite initializing state tests to invoke StreamManager.run() so the real finally block executes under test
- Replace hardcoded state list tests with assertions against ChannelState.PRE_ACTIVE frozenset
- Add missing SOURCE_BITRATE and FFMPEG_BITRATE constants to ChannelMetadataField (referenced by channel_status.py but never defined, causing AttributeError on detailed stats)
2026-03-15 19:39:15 -05:00
SergeantPanda
2d2b3e45f4 fix: harden ghost client cleanup and PRE_ACTIVE state guard
- Change ChannelState.PRE_ACTIVE from list to frozenset (immutable,
  O(1) membership tests, no risk of silent mutation)

- Add optional client_ids parameter to ClientManager.remove_ghost_clients()
  so callers that have already fetched SMEMBERS can pass it through,
  eliminating a redundant Redis round-trip

- Pass pre-fetched client_ids from get_basic_channel_info() into
  remove_ghost_clients() to remove the duplicate SMEMBERS call
2026-03-15 19:08:38 -05:00
None
40d0bc9567 fix: cap keepalive mode duration to prevent indefinite client hold
- Add MAX_KEEPALIVE_DURATION = 300s to TSConfig
- Track keepalive_start_time in _stream_data_generator; disconnect after cap is exceeded with a warning log
- Reset timer on real data so independent stalls don't accumulate
- Fix pre-existing test helper missing throttle attributes in NonOwnerWorkerKeepaliveTests
2026-03-15 14:22:57 -05:00
cmcpherson274
c8b665dc98
Update last_active timestamp in keepalive logic
Update last active timestamp for clients during failover.
2026-03-15 14:08:43 -04:00
cmcpherson274
10ea165c2c
Refactor packet handling with locking mechanism 2026-03-15 14:04:59 -04:00
cmcpherson274
f7085e5dc2
Add 'last_active' field to stats in stream_generator 2026-03-15 12:10:38 -04:00
cmcpherson274
fd9ca28316
Modify client TTL handling in client_manager.py
Updated client manager to refresh TTL without updating last_active timestamp.
2026-03-15 12:08:32 -04:00
cmcpherson274
cf1aef6747
Implement buffer reset method for stream transitions
Added a method to reset internal buffers to prevent corruption during stream transitions.
2026-03-15 12:06:32 -04:00
None
c65ace1a7c fix: stuck channel states and ghost clients in TS proxy
- Write ERROR via ownership check OR state guard fallback when ownership TTL expired during retries
- Add INITIALIZING to cleanup task grace period monitoring
- Validate client SET entries against metadata hashes in orphaned channel cleanup; remove ghosts and clean up when no real clients remain
- Stats page self-heals by removing ghost SET entries on read
- Extract remove_ghost_clients() helper to ClientManager
- Add ChannelState.PRE_ACTIVE constant
- Fix 6 pre-existing NonOwnerWorkerKeepalive test failures
2026-03-14 19:24:01 -05:00
dekzter
1dd368278c reverted value checking for consistency, added in user id to client info 2026-03-13 09:57:22 -04:00
dekzter
f69a462253 merged in dev 2026-03-13 08:22:44 -04:00
SergeantPanda
753bc1a893 fix(stream-preview): restore stream_name and stream_stats for stream previews
Stream.get_stream() was never called in the preview path of
generate_stream_url(), so the channel_stream and stream_profile
Redis keys were never written. This caused the stream_id to be
missing from channel metadata, which silently broke two things:

- Stats page showed no stream name or logo
- stream_stats were never saved to the database

Fix: replace the manual profile-selection loop in the preview path
with a direct call to Stream.get_stream(), matching the channel path
exactly. This also fixes a pre-existing non-atomic slot reservation
(read-then-check vs INCR-first) that could over-allocate connections
on concurrent previews.

Regression introduced in 49b7b9e2.
2026-03-08 11:46:31 -05:00
SergeantPanda
78aa8cf353 Enhancement: Change default new client behind seconds from 2 to 5 for stability. 2026-03-05 15:36:12 -06:00
SergeantPanda
9805732582 Merge branch 'dev' of https://github.com/Dispatcharr/Dispatcharr into pr/CodeBormen/1023 2026-03-05 11:16:20 -06:00
SergeantPanda
06f864ad6d fix(stream_generator): optimize resource and health checks with throttling 2026-03-04 17:37:42 -06:00
None
759688390b fix(dvr): fix series episode list, card layout, rule cleanup, and recording resilience
Backend:
- Add terminal status guard to stop() endpoint -prevents overwriting completed/interrupted/failed with "stopped" (returns 409 Conflict)
- Include currently-airing programs in series rule evaluation by filtering on end_time instead of start_time
- Clean up future recordings when deleting a series rule while preserving previously recorded episodes
- Add logo proxy negative cache to prevent repeated requests to unreachable hosts from blocking Daphne workers
- Add URL validation with HEAD-then-GET fallback and per-worker cache
- Add EPG time-slot matching (80% overlap threshold) to enrich manual recordings with program metadata
- Add multi-source poster resolution pipeline (EPG, VOD, TMDB, OMDb, TVMaze, iTunes) with URL validation at each stage
- Add editable recording metadata endpoint with user_edited flag to prevent EPG auto-overwrite
- Add refresh-artwork endpoint to re-run poster resolution on demand
- Batch-fetch PeriodicTask names in schedule_upcoming to avoid N+1

Frontend:
- Fix series modal showing only one episode — preserve _group_count from categorized prop when merging with store version so the episode list renders correctly
- Fix recording card button positioning — use flex column layout with marginTop auto so buttons align at the bottom regardless of content
- Add delete fallback in RecurringRuleModal for orphaned recordings from deleted rules
- Use end_time filter in upcoming episodes list to include currently-airing episodes
- Add inline metadata editing (title/description) in details modal
- Add refresh artwork button in details modal
- Replace hardcoded /logo.png fallbacks with imported default logo
- Add React.memo with custom comparator to RecordingCard

Tests:
- Add 21 EPG time-slot matching tests (overlap threshold, edge cases)
- Add 16 recording metadata tests (validation, artwork, logo cache)
- Add 17 URL validation tests (HEAD/GET fallback, caching, eviction)
2026-03-04 10:21:03 -06:00
None
e5206545dd fix(dvr): add stream reconnection resilience, fix comskip exit code handling
- Add reconnection loop to DVR streaming: on transient connectivity loss (ReadTimeout, ConnectionError, ChunkedEncodingError), the recording task retries the same TS proxy base up to 5 times with a 2-second delay, appending to the existing file. Counter resets on successful data receipt. TS container format tolerates brief discontinuities; MKV remux normalises timestamps.
- Extract _check_recording_cancelled() helper to consolidate repeated stop/delete DB checks across the streaming and reconnection paths.
- Fix comskip treating exit code 1 (no commercials detected) as a fatal error.
  Use check=False and handle return codes explicitly: 0 = commercials found,
  1 = no commercials (skipped), negative = signal, other = real error.
- Add human-readable reason messages for comskip 'skipped' notifications in
  WebSocket handler.
- Remove redundant conditional Channel import in stream_generator._cleanup() that shadowed the module-level import, causing UnboundLocalError when the conditional branch did not execute.
2026-03-04 10:21:02 -06:00