Skip to content

feat(container): update image ghcr.io/dispatcharr/dispatcharr ( 0.24.0 → 0.25.0 ) - #1769

Merged
Mafyuh merged 1 commit into
mainfrom
renovate/ghcr.io-dispatcharr-dispatcharr-0.x
May 23, 2026
Merged

feat(container): update image ghcr.io/dispatcharr/dispatcharr ( 0.24.0 → 0.25.0 )#1769
Mafyuh merged 1 commit into
mainfrom
renovate/ghcr.io-dispatcharr-dispatcharr-0.x

Conversation

@renovate

@renovate renovate Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Update Change
ghcr.io/dispatcharr/dispatcharr minor 0.24.00.25.0

Warning

Some dependencies could not be looked up. Check the Dependency Dashboard for more information.


Release Notes

Dispatcharr/Dispatcharr (ghcr.io/dispatcharr/dispatcharr)

v0.25.0

Compare Source

Security
  • Updated Django 6.0.4 → 6.0.5, resolving the following CVEs:
    • CVE-2026-6907: Cache leak exposing sensitive information via cache.get_or_set() race condition.
    • CVE-2026-35192: Persistent session cookies retaining sensitive information after logout.
    • CVE-2026-5766: Improper handling of length parameter inconsistency in multipart form parsing.
Added
  • About modal. A ? button in the sidebar footer opens an About dialog showing the current version, links to Documentation, Discord, GitHub, and Open Collective, a contributors acknowledgment, and a memorial note for Jesse Mann. The button is visible in both expanded and collapsed sidebar states.
  • Comskip mode setting. DVR Settings now includes a "Comskip mode" option:
    • Cut (default): FFmpeg permanently removes commercial segments from the recording file in place. The EDL file is deleted after a successful cut.
    • Mark: comskip analysis runs as normal but the recording file is left untouched. The EDL file is kept alongside the recording so players that support EDL-based commercial skipping (e.g. Kodi) can use it. The recording's custom_properties record the EDL filename, commercial count, and mode so the UI can surface this.
  • Comskip hardware acceleration setting. DVR Settings now includes a "Hardware acceleration" option that passes a hardware-decode flag to the comskip binary, reducing CPU load during commercial detection on capable hosts:
    • None (default): software decode.
    • NVIDIA NVDEC (--cuvid): requires the NVIDIA container toolkit and a supported GPU inside the container.
    • Intel Quick Sync (--qsv): requires an Intel iGPU or ARC GPU with the i915 driver exposed to the container.
  • HDHR output profile URL support. HDHomeRun lineup URLs now support an output_profile path segment so HDHR clients (Plex, Channels DVR, Emby, etc.) can request a specific transcode profile without any query-parameter support. URL formats accepted:
    • /hdhr/output_profile/<id>/lineup.json - output profile only
    • /hdhr/<channel_profile>/output_profile/<id>/lineup.json - channel profile + output profile
    • Bare /hdhr/lineup.json - existing behavior, uses the new system default (see below)
    • The profile ID is resolved against active OutputProfile rows; if the ID is inactive or does not exist, a warning is logged and the stream is served without transcoding.
  • HDHR default output profile setting. A new "HDHR Default Output Profile" select in Settings → Stream Settings lets admins pick an output profile that is applied to all HDHR stream URLs that do not specify one in the path. When cleared, streams are served as-is (pass-through).
  • fMP4 streaming support. Dispatcharr can now serve live channels as fragmented MP4 in addition to MPEG-TS. A dedicated FMP4RemuxManager runs a single FFmpeg remux process per active channel, muxes incoming TS data into fMP4 fragments, and stores them in a Redis-backed ring buffer. Multiple clients requesting the same channel in fMP4 all read from the same shared buffer, so only one FFmpeg process runs regardless of viewer count. Clients that request MPEG-TS continue to be served as before. The output format is selected via ?output_format=mpegts|fmp4 on the stream URL, or by the per-user default configured by an admin on the user account, or by the server-wide default in Stream Settings.
  • Output profiles. Admins can define named transcode profiles under Settings → Output Profiles. Each profile specifies an executable (e.g. ffmpeg) and a parameter string that reads raw TS from pipe:0 and writes the transcoded output to pipe:1. When a client requests a profile, one FFmpeg transcode process runs per active (channel, profile) pair and all requesting clients share the resulting output buffer. Common use case: a profile that converts AC3 audio to AAC for browser and mobile clients while the native stream (AC3 intact) continues to serve Plex/Emby/Jellyfin. Profiles are applied via ?output_profile=<id> on the stream URL, or by per-user and server-wide defaults. Two locked built-in profiles are seeded on first run: Media Server (AC3 Audio) (copies video, re-encodes audio to AC3 384k) and Web Player (AAC Audio) (copies video, re-encodes audio to AAC 192k). (Closes #​407)
  • Container format and output profile shown in Stats client rows. The expanded client row on the Stats page now displays the container format (mpegts or fmp4) and, when an output profile is active, the profile name.
  • Browser-local web player output profile setting. A new "Web Player Output Profile" select in Settings → UI Settings lets each browser choose which output profile (if any) is applied when previewing streams in the built-in floating player. The preference is saved to localStorage (dispatcharr-player-prefs) and picked up by every preview URL builder at click time, so a user whose account default transcodes to AC3 for their media server can still get a browser-compatible audio stream from the preview player without changing their account settings.
  • DVR series rules: EPG channel is now optional. Series recording rules no longer require a tvg_id. Rules with only a title or description filter search across all EPG channels in the 7-day horizon. The evaluator pre-loads one channel per EPG source (lowest channel number) in a single query and resolves the recording channel per-program, so cross-channel searches remain efficient. Saves and the preview endpoint now require at least one of title, or description instead of requiring tvg_id. The preview response includes a warn: true flag when more than 50 programs match, and the editor shows an orange alert when this flag is set. The upsert key for rules changed from tvg_id alone to (tvg_id, title) so multiple rules can target the same channel.
  • DVR series rules: rich title and description matching. Series recording rules now accept the same boolean / quoted-phrase / regex / whole-word semantics as the EPG Program Search API on both the title and description fields, plus an optional pinned channel. Existing rules continue to work unchanged (default title_mode=exact, no description filter, no pinned channel). (Closes #​570)
    • New rule fields: title_mode (exact / contains / search / regex), description, description_mode (contains / search / regex), and channel_id (optional integer to pin recordings to a specific channel; defaults to the lowest-numbered channel for the EPG as before).
    • The boolean/quoted/regex/whole-word parser was extracted from apps/epg/api_views.py into a shared apps/epg/query_utils.py so the rule evaluator and the search endpoint use the same code path. No behavioral changes to the existing /api/epg/programs/search/ endpoint.
    • The evaluator builds a single ProgramData queryset with .distinct() and reuses the existing (tvg_id, start_time, end_time) dedup key, so dropping in a description filter or a fuzzy title still preserves the dedup, episode collapsing, and offset-adjustment behavior. No N+1 introduced.
  • POST /api/channels/series-rules/preview/ returns up to 25 (configurable, max 100) upcoming programs that a candidate rule would match within the standard 7-day evaluation horizon, without persisting anything. Used by the new rule editor to give live feedback as the user types.
  • GET /api/epg/programs/search/?tvg_id= filter parameter for exact epg.tvg_id matches.
  • Series rule editor modal with form (title + match mode, description + match mode, episodes mode, pinned channel) and a debounced (500ms) preview pane backed by an AbortController so per-keystroke calls don't pile up. A "Customize rule..." link in the program record-choice modal opens the editor pre-filled with the program's tvg_id and title; the series rules modal gains an "Add rule" button and an "Edit" button per existing rule.
  • Shift+click and Ctrl+click row selection in tables. Clicking anywhere on a non-interactive area of a row now participates in selection:
    • Shift+click: extends selection from the last-clicked row to the current row (range select), identical to shift+clicking the checkbox.
    • Ctrl+click (Cmd+click on Mac): toggles the clicked row in or out of the current selection without disturbing other selected rows.
    • Plain clicks on action buttons, checkboxes, inputs, links, and menus are unaffected. The expand chevron uses stopPropagation so expanding a row does not also trigger selection.
Changed
  • Custom SVG icons extracted to icons.jsx. DiscordIcon and GitHubIcon were moved from PluginDetailPanel.jsx into a new shared frontend/src/components/icons.jsx module so they can be reused across components without cross-importing from an unrelated file.

  • Comskip .ini overhauled. The shipped docker/comskip.ini was replaced with a fully documented configuration covering all tunable sections: Main Settings, Output, Commercial Break Timing, Black Frame Detection, Logo Detection, Silence Detection, and Live TV. Key defaults: detect_method=127 (all seven detection methods, up from the comskip default of 107), min_commercialbreak=25 (slightly stricter floor for US broadcast TV), output_default=0 (suppresses the .txt stats file comskip writes by default), edl_skip_field=3 (Kodi commercial-break action code). All values include inline source references and plain-language explanations.

  • Comskip enable switch label updated. The DVR settings switch was relabeled from "Enable Comskip (remove commercials after recording)" to "Enable Comskip (commercial detection after recording)" to remain accurate when mark mode is selected.

  • Settings reorganization: Preferred Region and Auto-Import Mapped Files moved to System Settings. These two settings were previously stored in the stream_settings database group and shown under Stream Settings in the UI. They are now stored in system_settings and displayed under System Settings, which better reflects that they are server-wide behavior settings rather than stream delivery settings. A data migration (0025) moves existing values from the old group to the new one for all existing installs.

  • Stream Settings descriptions added. Default User Agent, Default Stream Profile, Default Output Format, M3U Hash Key, and HDHR Default Output Profile all now have inline description text below their labels explaining their purpose and effect.

  • System Settings descriptions added. Maximum System Events, Preferred Region, and Auto-Import Mapped Files now have inline description text. The redundant description paragraph that duplicated the accordion header text has been removed.

  • get_client_ip() in dispatcharr/utils.py cleaned up. Removed dead .split(',')[0] call and misleading variable name left over from a copy-paste of the X-Forwarded-For pattern. X-Real-IP is always a single IP (set by nginx from $remote_addr), so no splitting is needed. Behavior is unchanged.

  • ts_proxy module refactored and renamed to live_proxy. The live-streaming proxy was reorganized into a structured package (apps/proxy/live_proxy/) with explicit submodules for each stage of the pipeline: input/ (HTTP streamer, stream manager, TS ring buffer), output/ts/ (MPEG-TS client generator), output/fmp4/ (fMP4 remux manager, Redis buffer, client generator), and output/profile/ (output profile transcode manager). A dedicated redis_keys.py module centralizes all Redis key name construction for the proxy. Existing behavior for MPEG-TS clients is unchanged.

  • XC API allowed_output_formats now includes mp4. The user_info block returned by player_api.php and get.php previously advertised only ["ts"]. It now advertises ["ts", "mp4"] for all users, enabling XC-compatible clients that support fMP4 to request .mp4 stream URLs which are proxied as fMP4.

  • Browser preview URLs always force mpegts output. The four in-app preview buttons (Channels table, channel stream list, Streams table, Stats client row) now append ?output_format=mpegts (and the web player profile, if set) to the preview URL so the built-in mpegts.js player always receives a compatible stream, regardless of the user's configured default output format.

  • Series rules modal redesign. Rules now display a one-line summary with the title-match mode, description filter (when present), and a "Pinned channel" badge when a channel_id is configured. The previous list rendering remains for legacy rules.

  • EPG Program Search API (GET /api/epg/programs/search/): a new endpoint for querying EPG program data with rich filtering and query support. - Thanks @​northernpowerhouse

    • Text search on title and description with AND/OR boolean operators (case-insensitive), quoted phrase matching ("Law and Order" treats and as literal text), parenthetical grouping ((Newcastle OR NEW) AND (Villa OR AST)), whole-word mode (title_whole_words=true), and regex mode (title_regex=true).
    • Time filters: airing_at (programs live at a specific instant), start_after, start_before, end_after, end_before.
    • Relational filters: channel, channel_id, stream, group, epg_source.
    • Field selection: fields=title,start_time,channels returns only the requested keys; channel and stream data is skipped server-side (no wasted serialization) when not requested.
    • Pagination: default 50 results per page, configurable up to 500 via page_size.
    • Access control: results are scoped to channels the requesting user can access. user_level and the per-user adult-content filter are both enforced, matching the access model used by the M3U playlist, XC API, and stream proxy. Admin users receive unfiltered results.
    • Full OpenAPI/Swagger documentation available.
    • Requires IsStandardUser permission (user level ≥ 1).
  • Per-user IP/CIDR network allowlists. Admins can now assign IP address and CIDR range restrictions to individual user accounts via the API & XC tab on the user edit form. When a user has one or more allowed ranges configured, requests from IPs outside that list are rejected with 403 Forbidden regardless of the global network access policy; if no ranges are configured, the user inherits global settings unchanged. The existing network_access_allowed() utility is extended with an optional user argument so the per-user check is enforced at all access-controlled entry points (M3U/EPG, Streams, XC API, UI) without duplicating IP-matching logic. Per-user restrictions are stored in custom_properties['allowed_networks']; no model changes or migrations are required. — Thanks @​sethwv

  • reset_user_network management command: manage.py reset_user_network <username> clears the per-user allowed_networks restriction for the specified account, restoring it to global-policy inheritance. Useful for recovering a user locked out by a misconfigured allowlist. — Thanks @​sethwv

  • Auto-sync overhaul: comprehensive rebuild of the M3U auto-channel-sync flow. Introduces a per-field override system, hide-from-output flag, range-bounded auto-numbering with a re-pack helper, multi-stream channel safety, multi-provider shared-range merging, and an across-the-board move from per-row writes to bulk operations. (Closes #​1196) — Thanks @​CodeBormen

    • Per-field channel overrides for auto-synced channels. A new ChannelOverride table (one-to-one with Channel) holds user-specified values for name, channel_number, channel_group, logo, tvg_id, tvc_guide_stationid, epg_data, and stream_profile. Sync writes only to Channel.* columns; the override row takes precedence at read time via a new with_effective_values() queryset helper that coalesces both sources at the SQL layer. Every output surface that consumes channel data reads through this helper so overrides surface consistently: HDHR (/hdhr/lineup.json and per-profile variants, /hdhr/discover.json), M3U (/output/m3u, per-profile variants), EPG (/output/epg, per-profile variants), XC API (get_live_streams, xmltv.php), the channels list/detail endpoints, and the TV Guide summary endpoint (GET /api/channels/channels/summary/). Channel API responses now include both the raw name/channel_number/etc. fields AND new effective_* annotations plus the override object, so frontend consumers can show both "what the user picked" and "what the provider sent" simultaneously.
    • Per-field reset-to-provider icon. FK pickers (channel group, logo, EPG, stream profile) and scalar inputs (name, channel_number, tvg_id, tvc_guide_stationid) display a small reset icon ("Provider: " subtext + Undo2 button) when the field has an active override on an auto-synced channel. Clicking it sets the form value back to the provider value; the override for that field is cleared on save while other overrides remain intact.
    • Auto-created channel source attribution. The channel edit form shows an "Auto-created from: / " label at the top of auto-synced channels so the user can see which M3U account and which stream produced the row.
    • Hide-from-output flag (hidden_from_output). A user-toggleable boolean that excludes a channel from HDHR, M3U, EPG, and XC output without deleting it. Hidden channels are preserved across auto-sync refreshes and excluded from auto-cleanup. The channels table shows an EyeOff icon on hidden rows.
    • Channels table visibility filter and inline indicators. The table header has a new visibility selector ("Active Only" / "Hidden Only" / "Show All") plus a "Has Overrides" filter that narrows to channels with an active override row. Each row's name cell renders a yellow Pencil icon when overrides are active (tooltip lists the overridden field names) and an EyeOff icon when the channel is hidden.
    • Bulk hide / unhide in the bulk edit modal. Selecting any number of channels and toggling hidden_from_output in bulk edit applies the change in a single PATCH; auto-routes through the override-aware bulk endpoint so it works equivalently for auto-synced and manual channels.
    • Auto-sync channel ranges per group. ChannelGroupM3UAccount accepts auto_sync_channel_start (lower bound, inclusive) and auto_sync_channel_end (upper bound, inclusive; nullable for unbounded fill). Channels created by auto-sync are assigned numbers within the configured range; streams that do not fit are surfaced in the failure detail modal with a typed "RANGE_EXHAUSTED" reason.
    • Per-group inline configuration in the M3U Live group filter. Each group row now exposes a Sync toggle, a Numbering Mode selector (Provider / Next Available / Fixed), and Start/End channel-number inputs alongside the existing fields. The numbering modes control how each stream's channel number is chosen during sync: Provider tries the M3U-supplied number first then falls back to next-available; Next Available always picks the lowest free number from 1 upward; Fixed picks sequentially from the configured Start.
    • Per-group "Configure" modal. A gear-icon button on each group row opens a GroupConfigureModal containing the full set of advanced options. Fields surfaced in the modal: Channel Sort Order (provider order / name / tvg_id / updated_at, with reverse toggle), Compact Numbering toggle and "Re-pack now" button, Group Override (re-route this group's auto-created channels into a different ChannelGroup so multiple provider groups merge into one logical group), Name Regex Find/Replace pattern, Exclude Regex pattern, Force Dummy EPG, and a live regex preview pane.
    • Live regex preview pane. A new GET /api/channels/streams/regex-preview/ endpoint scans the group's streams (capped at 5000) for the find / match / exclude patterns the user is editing and returns up to 10 sample matches per pattern plus accurate total counts. The frontend debounces the call (500ms) and runs it inside the configure modal so the user sees live evidence of what their patterns will match before saving.
    • Live overlap warning across rows. As the user edits Start/End values across multiple groups in the same provider, an AbortController-debounced scan calls a new GET /api/channels/channels/numbers-in-range/ endpoint to detect (a) cross-row range overlaps (in-memory, all group pairs) and (b) channels already occupying the configured range that are not the expected occupants for that group. An informational warning surfaces inline; configurations are not blocked.
    • Compact numbering with re-pack helper. When compact_numbering=True on a group's account relation, sync packs visible auto-sync channels sequentially into the configured range. A "Re-pack now" button in the group config modal runs the pack manually via a new POST /api/m3u/accounts/{id}/repack-group/ endpoint. Hidden channels do not occupy slots; un-hiding shifts visible numbers down to fill the gap. Override-pinned channel numbers are respected as fixed anchors. Single-channel hide / unhide via the post-save signal triggers an incremental assign_compact_numbers_for_channels pass without requiring a full sync.
    • Multi-stream channel safety in sync. A user-attached second stream on an auto-created channel no longer causes the channel to be deleted when one of those streams disappears from the provider. The deletion path filters out channels where at least one current stream remains alive, and per-stream iteration mutates the same Channel instance so bulk_update writes the merged final state.
    • Orphan channel cleanup mode (3-state, account-scoped). Stored under M3UAccount.custom_properties.orphan_channel_cleanup and surfaced as a SegmentedControl at the top of the M3U account's group-settings page. Three positions: always (default; removes every auto-channel whose source stream has disappeared), preserve_customized (removes orphans without a ChannelOverride row, preserves those with one), and never (preserves all orphans, intended for users with manual redundancy/failover stream setups). Hidden channels are universally preserved regardless of mode.
    • Failure modal grouped by reason. The auto-sync completion notification's failure detail modal groups entries by typed reason (RANGE_EXHAUSTED, INTEGRITY_ERROR, OTHER) with collapsible sections and a per-group cap. The in-memory cap was raised from 50 to 1000 so realistic multi-provider failure sets are not truncated. The notification's auto-close timeout extends from 4s to 12s when failures are present so users have time to see and click into the modal.
    • Multi-provider shared-range merging. Two M3U accounts can target the same group with overlapping channel-number ranges. Sync seeds used_numbers globally so the second provider's channels pick up where the first left off without colliding. The overlap warning in the group-settings UI surfaces this configuration as informational; the merge is intentional, not a hard error.
    • Selection summary in the bulk edit modal. The bulk edit form shows Selection: X auto-synced, Y manual at the top so users can see how a single set of changes will route between override rows and direct writes.
    • Delete-Playlist preview. A new GET /api/m3u/accounts/{id}/auto-created-channels-count/ endpoint returns the count and up to 5 sample names of auto-created channels owned by the account. The Delete Playlist confirmation dialog uses this to show the user exactly how many channels will cascade away before they confirm.
    • Custom Channel.objects manager. A thin manager wraps the existing with_effective_values() helper so new code can call Channel.objects.with_effective_values(...) directly. The module-level helper remains the canonical implementation; existing call sites are unchanged.
  • Frontend unit tests added for form components. GroupManager, LiveGroupFilter, LoginForm, and Logo now have Vitest + Testing Library test suites covering rendering, user interactions, API integration, and edge cases (144 tests total). Business logic that was extracted into utility modules (ChannelGroupUtils, LiveGroupFilterUtils, LogoUtils, M3uUtils, M3uFilterUtils, M3uGroupFilterUtils, M3uProfileUtils, AutoSyncAdvancedUtils, AutoSyncBasicUtils) is also exercised through these tests. — Thanks @​nick4810

  • Global Network Access settings use tag-style inputs: the IP/CIDR range fields in the Network Access settings panel now use tag-style chip inputs instead of a plain text field, making it easier to add, review, and remove individual addresses or ranges. — Thanks @​sethwv

  • Auto-sync overhaul sync and channel behavior changes for the new override system. — Thanks @​CodeBormen

    • Channel.channel_number is now nullable. Auto-sync may produce channels without an assigned number (for example, a sync run where the configured range was exhausted before this stream could be slotted, or a hidden channel in compact mode whose slot was released for visible channels). Existing channels are unchanged. HDHR / M3U / EPG / XC output endpoints filter out channels with null channel_number, so a number-less row is invisible to clients but visible in the channels page where the user can assign one. The 0037 auto-sync overhaul migration includes a reverse-direction backfill on the channel_number AlterField step so a rollback that re-imposes NOT NULL still succeeds even if NULLs are present.
    • M3U account delete always cascades auto-created channels. The delete dialog is a single confirmation showing the count of affected channels (fetched from the new auto-created-channels-count endpoint). Auto-created channels are removed regardless of hidden_from_output or override state: override rows cascade via the one-to-one FK; hidden status does not preserve the channel because there is no provider left to populate it on the next sync. Manual channels are unaffected; their non-provider streams remain intact, and only streams owned by the deleted account are removed. The destroy response now returns {"deleted_channels": N} (HTTP 200) instead of an empty 204 so the confirmation toast can show the actual count.
    • Bulk channel edit auto-routes auto-created channels through the override path. When a bulk edit includes auto-synced channels, the changed fields are written to each channel's ChannelOverride row instead of the raw Channel.* columns, so the edit survives the next sync. Manual channels in the same selection write directly to Channel.*. A "Clear all overrides" affordance pre-clears overrides on the selection before applying new edits in the same submit (clear runs before the routing PATCH so a same-submit clear cannot wipe just-written overrides).
    • Inline edits on the channels table route through the override row for auto-synced channels. Editing a cell directly in the table (number, name, group, EPG, logo) now writes to ChannelOverride.<field> for auto-synced rows so the edit survives the next refresh; manual rows continue to write directly. The save mechanic and validation are unchanged from the user's perspective.
    • Channel-number duplicates are explicitly allowed. Sync's used_numbers seed still avoids assigning the same number to two newly-created auto channels in the same run, so accidental duplication during sync remains impossible. Manual or override-driven duplication is permitted; downstream client behavior on duplicates varies by client.
  • gevent cooperative multitasking enabled in all uWSGI workers. gevent-early-monkey-patch = true and import = dispatcharr.gevent_patch are now set in all four uWSGI configuration files (uwsgi.ini, uwsgi.modular.ini, uwsgi.dev.ini, uwsgi.debug.ini). The new dispatcharr/gevent_patch.py module ensures gevent's stdlib monkey-patching is applied before application code loads (replacing blocking socket, threading, and OS primitives with cooperative gevent equivalents) and installs psycogreen's wait-callback so psycopg2 database I/O yields to the gevent hub instead of blocking the OS thread. Without these settings, any blocking psycopg2, requests, or DNS call froze every greenlet on the affected worker for the duration of the call.

  • WebSocket group sends rewritten to bypass asyncio in gevent workers. gevent.monkey.patch_all() removes select.epoll from the stdlib select module, which breaks asyncio event loop creation in threadpool threads. The previous send_websocket_update and _send_async paths dispatched via async_to_sync(channel_layer.group_send)() from a threadpool thread, which failed with AttributeError: module 'select' has no attribute 'epoll'; the exception was caught at WARNING level, so every WebSocket push from REST views was silently discarded. A new _gevent_ws_send() in core/utils.py replicates the channels_redis 4.x group_send wire format directly using the synchronous Redis client - group membership lookup from the asgi:group:{name} sorted set, msgpack serialization with a 12-byte random prefix, and ZADD to per-channel sorted sets. Both send_websocket_update() and _send_async() detect gevent patching at call time and dispatch via gevent.spawn(_gevent_ws_send) instead. Celery workers, which are not gevent-patched, continue to use the async_to_sync path unchanged.

  • Dependency updates:

    • Django 6.0.4 → 6.0.5 (security patch; see Security section)
Removed
  • python-gnupg dependency dropped. GPG manifest signature verification now calls the gpg binary directly via os.posix_spawn (see Fixed below). The python-gnupg Python library was the only consumer and has been removed from pyproject.toml. The gpg binary itself is still required on the host (it was always required since python-gnupg is just a wrapper around it).
Fixed
  • DVR settings form no longer flashes back to old values during save. The comskip mode and hardware acceleration selects briefly showed stale values while the save was in flight because the Zustand settings store update (triggered by the API response) fired the useEffect([settings]) re-hydration hook mid-save. An isSavingRef guard now suppresses the reactive re-hydration while a save is in progress; after a successful save the form is explicitly synced from the freshly-updated store state instead.
  • _cleanup_local_resources skipped ClientManager.stop() on channel removal. ProxyServer._cleanup_local_resources deleted each channel's ClientManager entry with del, which removed it from the dict but gave the object no signal to terminate. The per-channel heartbeat greenlet inside the manager continued running until it next checked its running flag and found the channel absent from Redis. The entry is now removed with pop() and stop() is called on the captured manager before it is discarded, terminating the heartbeat immediately on channel cleanup.
  • XC profile exp_date not updating on account refresh. refresh_account_profiles saved the freshly-fetched custom_properties with update_fields=['custom_properties'], which excluded exp_date from the SQL UPDATE. The model's save() method parses the new expiry from custom_properties and assigns it to self.exp_date, but that value was silently dropped because the column was not listed in update_fields. Added 'exp_date' to the update_fields list so both columns are written together.
  • ~25-second transcode startup delay and worker freeze when starting ffmpeg under gevent+uWSGI. Enabling gevent cooperative multitasking (see Changed above) exposed a deadlock: fork() hangs indefinitely in gevent's _before_fork pthread_atfork handler when called from any thread while gevent is running - including from real OS threads. subprocess.Popen, which all three ffmpeg spawn sites used, calls fork() internally, stalling the uWSGI worker for ~25 seconds or freezing it entirely and blocking all other clients on that worker.
    • input/manager.py: replaced subprocess.Popen with os.posix_spawn + a minimal _SpawnedProcess wrapper. os.posix_spawn is POSIX-specified to skip pthread_atfork handlers entirely.
    • output/fmp4/manager.py, output/profile/manager.py: replaced subprocess.Popen with a new shared posix_spawn_proc() helper in live_proxy/utils.py. The helper also sets O_NONBLOCK on the stdin pipe write-end: under gevent, threading.Thread is monkey-patched to greenlets, so a blocking write to a full 64 KB pipe would stall the entire hub. _write_all() in both output managers now treats a None return (EAGAIN on the non-blocking FD) as a cooperative wait via select.select() rather than a fatal error.
    • input/http_streamer.py: set O_NONBLOCK on the HTTP-to-pipe relay write-end with an EAGAIN retry loop for the same reason.
    • core/views.py (stream_view): replaced subprocess.Popen with os.posix_spawn; also fixed a pre-existing indentation bug where the return StreamingHttpResponse(...) was accidentally nested inside stream_generator (making every successful response return None and raise a Django error). Also corrected two NameError references to the undefined stream_id variable in log messages.
    • apps/connect/handlers/script.py (ScriptHandler): replaced subprocess.run with a _posix_run helper that uses os.posix_spawn + cooperative select.select reads + non-blocking waitpid polling. Without this fix, any script-type connect integration configured for events fired from a uWSGI worker (e.g. client_connect) would deadlock the serving greenlet. Note: cwd is no longer set to the script's directory during execution (it inherits the worker's cwd); this was a minor convenience, not a documented guarantee.
  • POST /api/plugins/repos/plugin-detail/ hung for up to 105 seconds under gevent+uWSGI. The plugin detail endpoint called GPG via subprocess.Popen to verify per-plugin manifest signatures, triggering the same fork() atfork deadlock described above. Replaced with a _gpg_run() helper that uses os.posix_spawn, matching the pattern used by the ffmpeg and script-handler fixes. select.select() drains stdout/stderr cooperatively (gevent-patched) and os.waitpid(WNOHANG) with time.sleep(0.01) reaps the child without blocking the hub. Results are cached in Redis for 5 minutes per manifest URL so repeat detail fetches skip GPG entirely. The cache is also invalidated per-plugin when the owning repo's hub manifest is refreshed, so a newly released version is visible immediately after a manual hub refresh.
  • XC server sub-path URLs now work correctly. When a provider serves its XC API from a sub-path (e.g. http://server/Pluto/gb/player_api.php), Dispatcharr was stripping the path entirely and hitting the root (/player_api.php) instead. _normalize_url now preserves sub-path components and only strips any trailing .php segment (covering player_api.php, get.php, xmltv.php, and any future endpoint without a maintained list). The same fix is applied to get_transformed_credentials in the M3U profile transformation path. (Fixes #​1218)
  • M3U filter delete confirmation showed wrong field name and had a typo. The confirmation dialog for deleting an M3U filter read filter.type (always undefined) instead of filter.filter_type, leaving the "Type:" line blank, and displayed "Patter:" instead of "Pattern:". Both are corrected. — Thanks @​nick4810
  • M3U form FileInput expanded the modal width on long filenames. Uploading a local M3U file with a long name caused the FileInput to expand beyond the modal's layout bounds. The input now clips overflow with textOverflow: ellipsis. — Thanks @​nick4810
  • Login loading spinner not cleared on successful login. setIsLoading(false) was inside the catch block only, so a successful login that immediately navigated away left the loading state as true if the component re-mounted. Moved to a finally block so it always resets. — Thanks @​nick4810
  • Bulk channel edit silently failed on API rejection. The bulk-edit submit handler in ChannelBatch.jsx only wrote console.error when the PATCH was rejected; the user saw the spinner stop but received no notification, leading them to assume the save succeeded. The catch block now surfaces a red toast with the server-provided detail (or a generic fallback), and the form stays open with the in-progress selection intact so the user can correct and retry. — Thanks @​CodeBormen
  • Recurring rule edit modal showed a blank Channel field. The RecurringRuleModal read channel data from a Zustand store slice (channels) that nothing populates - the channels page and all other consumers switched to on-demand summary fetches in a prior release. Opening the edit modal from the DVR page always produced an empty Channel select. The modal now fetches channels via API.getChannelsSummary() when it opens, matching the lightweight approach used by the one-time recording form and other modals.
  • DVR section count badges appeared at the far right of the screen. The "Currently Recording", "Upcoming Recordings", and "Previously Recorded" section headers wrapped the title and badge in a Group justify="space-between", which pushed the badge to the opposite edge of the full-width container. Changed to Group gap="xs" align="center" so each badge sits inline with its heading.
  • Plugin event dispatch aborted silently on first disabled plugin. trigger_event in apps/connect/utils.py iterated pm.list_plugins() and, for disabled plugins, logged a debug message using plugin.key / plugin.name (attribute access). Because list_plugins() returns dicts, this raised AttributeError on the first disabled plugin encountered. Fixed by changing the two accesses to plugin['key'] / plugin['name']. (Fixes #​1231) - Thanks @​R3XCHRIS
  • Plugin periodic tasks silently missed the first beat tick after every Celery worker restart. Plugin modules live outside INSTALLED_APPS so autodiscover_tasks() never imports them, and worker startup skips plugin discovery via should_skip_initialization(). Any plugin using module-level @shared_task had its tasks unregistered with the worker until a lazy event import warmed the module; beat fired on schedule but the worker rejected with Received unregistered task and advanced last_run_at anyway, hiding the miss. A worker_ready hook in dispatcharr/celery.py now eagerly calls PluginManager.discover_plugins(sync_db=False) on every worker boot so plugin tasks are registered before beat starts firing. (Fixes #​1244) - Thanks @​R3XCHRIS
  • Plugin monkey-patches and module-level hooks were never applied in uWSGI workers. Both uWSGI configs use lazy-apps=true, meaning each worker boots independently and never inherits state from the master. should_skip_initialization() correctly skipped one-shot startup tasks in workers, but also blocked discover_plugins, so plugin modules were never imported in any of the 4 request-serving workers. Plugins relying on patching request handling (monkey-patches, signal registrations) were silently inactive until a Connect event lazily triggered discovery in that specific worker. Discovery is now run in every process that serves requests, gated only for Celery processes (which use worker_ready) and management commands that don't serve requests.
  • PostgreSQL connection pool exhausted under load in gevent workers. uWSGI's gevent pool runs many greenlets concurrently on a single OS thread. With CONN_MAX_AGE = 60, each greenlet that touched the database retained its own open connection for the full 60 seconds (gevent thread-locals are greenlet-locals), rapidly exhausting PostgreSQL's max_connections limit under moderate concurrency. DATABASE_CONN_MAX_AGE is now 0 so connections are closed after each request. close_old_connections() is also called at the top of the long-running stream manager and cleanup watchdog loops so stale handles accumulated across greenlet switches are released promptly.
  • Stream proxy race: cleanup watchdog could stop a channel still in the connecting phase. When the first viewer triggered channel initialization, the client was registered only after the connect-wait loop completed. The cleanup watchdog runs concurrently and stops channels with zero connected clients after a grace period; if the grace period elapsed during the connect-wait, the watchdog killed the channel and left the viewer stuck. The client is now registered before the connect-wait loop begins so the watchdog always sees at least one client.
  • 2-second "stream thread did not terminate within timeout" warning on every channel stop. _close_socket in input/manager.py was closing the relay pipe read-end (self.socket) before killing the ffmpeg process. The stream OS thread blocks in select() on that fd; on Linux, closing an fd from another thread while a select() is in progress on it does not reliably interrupt the call (POSIX allows this to be undefined). The thread stayed blocked for the full chunk-timeout (5 s), and stream_thread.join(timeout=2.0) always expired first. Fixed by killing ffmpeg first: when ffmpeg dies its copy of the relay write-end closes, delivering EOF to select() immediately. self.socket is closed afterward as cleanup only.
  • Duplicate stop_channel calls on every channel shutdown. StreamGenerator._cleanup triggered channel shutdown via two parallel paths: client_manager.remove_client() (which fires handle_client_disconnect on the owning worker, the correct path) and _schedule_channel_shutdown_if_needed (which independently spawned a delayed stop_channel greenlet). The duplicate call was suppressed by the _stopping_channels guard but produced a redundant log entry and unnecessary greenlet on every shutdown. _schedule_channel_shutdown_if_needed has been removed; handle_client_disconnect is the sole shutdown trigger.
  • Concurrent greenlets could re-enter _close_socket during proc.wait(). self.transcode_process was set to None at the end of the if proc: block rather than immediately after capturing the reference. Under gevent, proc.wait(timeout=0.5) yields the hub, allowing a second greenlet to enter _close_socket, find self.transcode_process still set, and attempt a second kill+close. self.transcode_process = None is now assigned immediately after proc = self.transcode_process so concurrent callers see None and skip the block.
  • Channel card "started at" tooltip jumping by 1 second on every stats poll. The Stats page channel card tooltip showed the channel start time by computing Date.now() - uptime * 1000 on every render. Because uptime is a server-side elapsed-seconds value recomputed each response, this reconstruction drifted by up to 1 second per tick. The basic channel info path now emits started_at (the raw Unix timestamp from Redis) alongside uptime, matching what the detailed stats path already sent. The frontend getStartDate helper now accepts the stable started_at timestamp directly, so the displayed wall-clock time is fixed from the first poll and never changes.
  • Shift+click range selection in the channels table broken after row memoization. After MemoizedTableRow was introduced, handleShiftSelect captured lastClickedId from its render-time closure. Because the memo comparator intentionally excludes callback function references, unselected rows retained the stale closure where lastClickedId === null, so every shift+click from a previously unchecked row fell through to a plain toggle instead of selecting the range. Added lastClickedIdRef and allRowIdsRef alongside the existing selectedTableIdsRef; handleShiftSelect now reads from those refs so every row uses the current anchor ID and full ID list regardless of which render produced the closure.
  • Selected rows in the channels table did not show the teal highlight. MemoizedTableRow applied backgroundColor: '#&#8203;163632' based on row.getIsSelected() from TanStack Table's API. Because state.rowSelection was never wired into useReactTable, row.getIsSelected() always returned false and selected rows remained unstyled. Changed to use the isSelected prop, which is correctly derived from selectedTableIdsSet and already tracked by the memo comparator.
  • blur event listener in useTable leaked on component unmount. The useEffect cleanup function called window.removeEventListener('blur', ...) with a newly created anonymous function literal that never matched the handler registered at setup time, so the listener was never removed. Extracted to a named handleBlur constant so setup and cleanup reference the same function.
Performance
  • EPG HTTP response cache replaced with django-redis. The default Django cache backend was LocMemCache, an in-process memory store. With multiple uWSGI workers, each worker independently generated and cached the full EPG XML in its own heap; with 4 workers the peak memory cost was up to 4 times the size of the EPG document. The cache backend is now django_redis.cache.RedisCache, backed by the same Redis instance used by channels_redis, so a single cached EPG copy is shared across all workers.
  • AutoSyncAdvanced and LogoForm are now lazy-loaded in the M3U group filter. Both components are large and only needed when the user opens the gear modal or logo upload modal. Wrapping them in React.lazy + Suspense removes them from the initial bundle and defers their parse/execute cost until first use. — Thanks @​nick4810
  • Auto-sync at scale: the new override-aware sync flow is more capable than the prior path but the implementation choices below keep it viable on libraries with thousands of channels. — Thanks @​CodeBormen
    • Bulk writes throughout sync_auto_channels. Per-row Channel.objects.create() and .save() calls were replaced with bulk_create() and bulk_update() paths that batch the entire group's create + update sets into single round-trips. The renumber pass collects all dirty channels into one list and flushes with a single bulk_update at the end of the loop. ChannelStream.order writes were similarly consolidated into a single bulk_update.
    • Single-pass collision detection via a global used_numbers set. Choosing a free channel number was previously an O(N) DB query per stream. The new path seeds a set() of all reserved numbers (existing channels + override pins) once per run; _next_available_number() is O(cluster size) against that set. The seed deliberately excludes only this account's visible auto-created channels (the rows about to be reassigned), so cross-account and hidden-channel reservations are honored without extra queries.
    • SQL-level effective-value coalescing via with_effective_values(). All channel-data consumers (HDHR, M3U, EPG, XC, channel list / detail, TV Guide summary) read effective values directly from a single annotated queryset that left-joins the override row at the database layer. Prevents N+1 ChannelOverride lookups across the channel scan and lets the same query serve the join.
    • EPG dispatch deduplication. bulk_create and bulk_update bypass post_save, so the post-loop step explicitly dispatches parse_programs_for_tvg_id.delay once per unique epg_data_id actually changed in the run, not per channel. Avoids fan-out where one EPG source change otherwise queued thousands of redundant parse tasks.
    • Logo and EPGData lookup caching during sync. sync_auto_channels now caches Logo and EPGData lookups in a per-run dict so repeated provider URLs / IDs across many streams do not produce duplicate Logo.objects.get_or_create or EPGData.objects.filter calls.
    • Override-aware bulk PATCH endpoint. update_channels_with_override_routing partitions the bulk-edit selection into auto-created (override write) vs manual (direct write) once, then issues two bulk PATCHes instead of N single-row updates.
  • xc_get_live_streams response-build overhead reduced. Previously the function iterated the full channel queryset three times (two passes for collision-free integer number mapping, one pass to build the response list) and called reverse() and build_absolute_uri_with_port() once per channel to construct logo URLs, amounting to N URL-pattern lookups and N header-parse calls per request. The number-mapping passes are now combined into one full queryset pass that immediately classifies channels with integer numbers, deferring only the fractional-number subset to a second, smaller loop. Logo URL construction is precomputed once per request using a single reverse() call; per-channel URLs are assembled with string interpolation. _get_default_group_id() is also called at most once per null-group channel instead of twice. The get_live_streams API endpoint now returns a StreamingHttpResponse backed by a generator that serializes one channel entry at a time instead of building the full JSON array in memory before writing; time-to-first-byte is lower on large channel libraries and peak worker memory is O(1) per channel rather than O(N). (Fixes #​1220)
  • Stream filter-options fast path when no filters are active. GET /api/channels/streams/filter-options/ previously ran DISTINCT queries across the full streams table while also mutating the underlying Django request object to strip inapplicable filter params. With no active filters the fast path now queries the streams table directly with two simple DISTINCT aggregations, skipping the request mutation and filterset instantiation overhead.
  • Channel list queryset drops unconditional DISTINCT. ChannelViewSet.get_queryset() previously appended .distinct() unconditionally. DISTINCT is only needed when the query joins a one-to-many table - specifically when channel_profile_id or only_stale filters are active. The queryset now returns plain results in the common case, eliminating sort-and-deduplicate overhead on most channel list fetches.
  • JsonResponse for ID list and channel summary endpoints. The stream_ids, channel_ids, and channel summary endpoints now use django.http.JsonResponse instead of DRF's Response, bypassing the DRF renderer pipeline (content-type negotiation, serializer dispatch) for responses that are already plain Python structures. Removes overhead on every channel-table load and stream-table load.
  • Logo queryset annotates channel_count to eliminate N+1 in LogoSerializer. LogoViewSet.get_queryset() now annotates each row with Count('channels'). LogoSerializer.get_channel_count() and get_is_used() read the annotation directly instead of issuing a separate COUNT(*) per logo. The used=true and used=false list filters use the annotation for their conditions, removing the DISTINCT that was previously required.
  • ChannelProfileSerializer reads prefetched memberships. ChannelProfileViewSet.get_queryset() now prefetches enabled ChannelProfileMembership rows into enabled_memberships. ChannelProfileSerializer.get_channels() uses the prefetched set when available, eliminating one query per profile in any response that lists multiple profiles.
  • Channel table selection re-render reduction.
    • Removed isShiftKeyDown React state from useTable. Setting it on every keydown/keyup event triggered a re-render of any table consumer (ChannelsTable, CustomTableBody, ~50 memoized row comparisons) each time shift was pressed or released. The visual shift-key effect is handled entirely by document.body.classList manipulation, so the React state served no purpose.
    • Removed the selectedChannelIds reactive Zustand store subscription from ChannelsTable. Each checkbox click wrote to both local selectedTableIds state and the store via onRowSelectionChange, causing two sequential re-renders of ChannelsTable per click. The two consumers of this subscription (deleteChannel and ChannelBatchForm) now read from table.selectedTableIds directly.
    • Removed the dead rowSelection useMemo that built a TanStack row-selection map from selectedTableIds. The map was placed in tableInstance but state.rowSelection was never passed to useReactTable, so TanStack never consumed it. Eliminated a full page-row iteration on every selection change.

Configuration

📅 Schedule: (in timezone America/New_York)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about these updates again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@github-actions

Copy link
Copy Markdown
--- kubernetes/apps/arr/dispatcharr/app Kustomization: arr/dispatcharr HelmRelease: arr/dispatcharr

+++ kubernetes/apps/arr/dispatcharr/app Kustomization: arr/dispatcharr HelmRelease: arr/dispatcharr

@@ -39,13 +39,13 @@

               DISPATCHARR_LOG_LEVEL: info
               REDIS_HOST: localhost
               TZ: null
             image:
               pullPolicy: IfNotPresent
               repository: ghcr.io/dispatcharr/dispatcharr
-              tag: 0.24.0
+              tag: 0.25.0
             probes:
               liveness:
                 enabled: false
             resources:
               limits:
                 memory: 2Gi

@github-actions

Copy link
Copy Markdown
--- HelmRelease: arr/dispatcharr Deployment: arr/dispatcharr

+++ HelmRelease: arr/dispatcharr Deployment: arr/dispatcharr

@@ -42,13 +42,13 @@

         - name: DISPATCHARR_LOG_LEVEL
           value: info
         - name: REDIS_HOST
           value: localhost
         - name: TZ
           value: null
-        image: ghcr.io/dispatcharr/dispatcharr:0.24.0
+        image: ghcr.io/dispatcharr/dispatcharr:0.25.0
         imagePullPolicy: IfNotPresent
         name: app
         resources:
           limits:
             memory: 2Gi
           requests:

@Mafyuh
Mafyuh merged commit a1d693e into main May 23, 2026
5 checks passed
@Mafyuh
Mafyuh deleted the renovate/ghcr.io-dispatcharr-dispatcharr-0.x branch May 23, 2026 02:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant