feat(container): update image ghcr.io/dispatcharr/dispatcharr ( 0.24.0 → 0.25.0 ) - #1769
Merged
Merged
Conversation
--- 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 |
--- 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: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
0.24.0→0.25.0Warning
Some dependencies could not be looked up. Check the Dependency Dashboard for more information.
Release Notes
Dispatcharr/Dispatcharr (ghcr.io/dispatcharr/dispatcharr)
v0.25.0Compare Source
Security
Django6.0.4 → 6.0.5, resolving the following CVEs:cache.get_or_set()race condition.Added
?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.custom_propertiesrecord the EDL filename, commercial count, and mode so the UI can surface this.--cuvid): requires the NVIDIA container toolkit and a supported GPU inside the container.--qsv): requires an Intel iGPU or ARC GPU with the i915 driver exposed to the container.output_profilepath 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/hdhr/lineup.json- existing behavior, uses the new system default (see below)OutputProfilerows; if the ID is inactive or does not exist, a warning is logged and the stream is served without transcoding.FMP4RemuxManagerruns 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|fmp4on 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.ffmpeg) and a parameter string that reads raw TS frompipe:0and writes the transcoded output topipe: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)mpegtsorfmp4) and, when an output profile is active, the profile name.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.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 oftitle, ordescriptioninstead of requiringtvg_id. The preview response includes awarn: trueflag when more than 50 programs match, and the editor shows an orange alert when this flag is set. The upsert key for rules changed fromtvg_idalone to(tvg_id, title)so multiple rules can target the same channel.title_mode=exact, no description filter, no pinned channel). (Closes #570)title_mode(exact/contains/search/regex),description,description_mode(contains/search/regex), andchannel_id(optional integer to pin recordings to a specific channel; defaults to the lowest-numbered channel for the EPG as before).apps/epg/api_views.pyinto a sharedapps/epg/query_utils.pyso the rule evaluator and the search endpoint use the same code path. No behavioral changes to the existing/api/epg/programs/search/endpoint.ProgramDataqueryset 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 exactepg.tvg_idmatches.AbortControllerso 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'stvg_idand title; the series rules modal gains an "Add rule" button and an "Edit" button per existing rule.stopPropagationso expanding a row does not also trigger selection.Changed
Custom SVG icons extracted to
icons.jsx.DiscordIconandGitHubIconwere moved fromPluginDetailPanel.jsxinto a new sharedfrontend/src/components/icons.jsxmodule so they can be reused across components without cross-importing from an unrelated file.Comskip
.inioverhauled. The shippeddocker/comskip.iniwas 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.txtstats 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_settingsdatabase group and shown under Stream Settings in the UI. They are now stored insystem_settingsand 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()indispatcharr/utils.pycleaned up. Removed dead.split(',')[0]call and misleading variable name left over from a copy-paste of theX-Forwarded-Forpattern.X-Real-IPis always a single IP (set by nginx from$remote_addr), so no splitting is needed. Behavior is unchanged.ts_proxymodule refactored and renamed tolive_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), andoutput/profile/(output profile transcode manager). A dedicatedredis_keys.pymodule centralizes all Redis key name construction for the proxy. Existing behavior for MPEG-TS clients is unchanged.XC API
allowed_output_formatsnow includesmp4. Theuser_infoblock returned byplayer_api.phpandget.phppreviously advertised only["ts"]. It now advertises["ts", "mp4"]for all users, enabling XC-compatible clients that support fMP4 to request.mp4stream URLs which are proxied as fMP4.Browser preview URLs always force
mpegtsoutput. 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-inmpegts.jsplayer 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_idis 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"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).airing_at(programs live at a specific instant),start_after,start_before,end_after,end_before.channel,channel_id,stream,group,epg_source.fields=title,start_time,channelsreturns only the requested keys; channel and stream data is skipped server-side (no wasted serialization) when not requested.page_size.user_leveland 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.IsStandardUserpermission (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 Forbiddenregardless of the global network access policy; if no ranges are configured, the user inherits global settings unchanged. The existingnetwork_access_allowed()utility is extended with an optionaluserargument 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 incustom_properties['allowed_networks']; no model changes or migrations are required. — Thanks @sethwvreset_user_networkmanagement command:manage.py reset_user_network <username>clears the per-userallowed_networksrestriction for the specified account, restoring it to global-policy inheritance. Useful for recovering a user locked out by a misconfigured allowlist. — Thanks @sethwvAuto-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
ChannelOverridetable (one-to-one withChannel) holds user-specified values forname,channel_number,channel_group,logo,tvg_id,tvc_guide_stationid,epg_data, andstream_profile. Sync writes only toChannel.*columns; the override row takes precedence at read time via a newwith_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.jsonand 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 rawname/channel_number/etc. fields AND neweffective_*annotations plus theoverrideobject, so frontend consumers can show both "what the user picked" and "what the provider sent" simultaneously.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.hidden_from_outputin 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.ChannelGroupM3UAccountacceptsauto_sync_channel_start(lower bound, inclusive) andauto_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.GroupConfigureModalcontaining 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 differentChannelGroupso 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.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.AbortController-debounced scan calls a newGET /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=Trueon 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 newPOST /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 incrementalassign_compact_numbers_for_channelspass without requiring a full sync.Channelinstance sobulk_updatewrites the merged final state.M3UAccount.custom_properties.orphan_channel_cleanupand 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 aChannelOverriderow, preserves those with one), andnever(preserves all orphans, intended for users with manual redundancy/failover stream setups). Hidden channels are universally preserved regardless of mode.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.used_numbersglobally 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: X auto-synced, Y manualat the top so users can see how a single set of changes will route between override rows and direct writes.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.Channel.objectsmanager. A thin manager wraps the existingwith_effective_values()helper so new code can callChannel.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, andLogonow 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 @nick4810Global 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_numberis 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 withnullchannel_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 thechannel_numberAlterField step so a rollback that re-imposes NOT NULL still succeeds even if NULLs are present.hidden_from_outputor 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.ChannelOverriderow instead of the rawChannel.*columns, so the edit survives the next sync. Manual channels in the same selection write directly toChannel.*. 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).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.used_numbersseed 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 = trueandimport = dispatcharr.gevent_patchare now set in all four uWSGI configuration files (uwsgi.ini,uwsgi.modular.ini,uwsgi.dev.ini,uwsgi.debug.ini). The newdispatcharr/gevent_patch.pymodule 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()removesselect.epollfrom the stdlibselectmodule, which breaks asyncio event loop creation in threadpool threads. The previoussend_websocket_updateand_send_asyncpaths dispatched viaasync_to_sync(channel_layer.group_send)()from a threadpool thread, which failed withAttributeError: 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()incore/utils.pyreplicates the channels_redis 4.xgroup_sendwire format directly using the synchronous Redis client - group membership lookup from theasgi:group:{name}sorted set, msgpack serialization with a 12-byte random prefix, andZADDto per-channel sorted sets. Bothsend_websocket_update()and_send_async()detect gevent patching at call time and dispatch viagevent.spawn(_gevent_ws_send)instead. Celery workers, which are not gevent-patched, continue to use theasync_to_syncpath unchanged.Dependency updates:
Django6.0.4 → 6.0.5 (security patch; see Security section)Removed
python-gnupgdependency dropped. GPG manifest signature verification now calls thegpgbinary directly viaos.posix_spawn(see Fixed below). Thepython-gnupgPython library was the only consumer and has been removed frompyproject.toml. Thegpgbinary itself is still required on the host (it was always required sincepython-gnupgis just a wrapper around it).Fixed
useEffect([settings])re-hydration hook mid-save. AnisSavingRefguard 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_resourcesskippedClientManager.stop()on channel removal.ProxyServer._cleanup_local_resourcesdeleted each channel'sClientManagerentry withdel, 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 withpop()andstop()is called on the captured manager before it is discarded, terminating the heartbeat immediately on channel cleanup.exp_datenot updating on account refresh.refresh_account_profilessaved the freshly-fetchedcustom_propertieswithupdate_fields=['custom_properties'], which excludedexp_datefrom the SQLUPDATE. The model'ssave()method parses the new expiry fromcustom_propertiesand assigns it toself.exp_date, but that value was silently dropped because the column was not listed inupdate_fields. Added'exp_date'to theupdate_fieldslist so both columns are written together.fork()hangs indefinitely in gevent's_before_forkpthread_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, callsfork()internally, stalling the uWSGI worker for ~25 seconds or freezing it entirely and blocking all other clients on that worker.input/manager.py: replacedsubprocess.Popenwithos.posix_spawn+ a minimal_SpawnedProcesswrapper.os.posix_spawnis POSIX-specified to skip pthread_atfork handlers entirely.output/fmp4/manager.py,output/profile/manager.py: replacedsubprocess.Popenwith a new sharedposix_spawn_proc()helper inlive_proxy/utils.py. The helper also setsO_NONBLOCKon the stdin pipe write-end: under gevent,threading.Threadis 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 aNonereturn (EAGAIN on the non-blocking FD) as a cooperative wait viaselect.select()rather than a fatal error.input/http_streamer.py: setO_NONBLOCKon the HTTP-to-pipe relay write-end with an EAGAIN retry loop for the same reason.core/views.py(stream_view): replacedsubprocess.Popenwithos.posix_spawn; also fixed a pre-existing indentation bug where thereturn StreamingHttpResponse(...)was accidentally nested insidestream_generator(making every successful response returnNoneand raise a Django error). Also corrected twoNameErrorreferences to the undefinedstream_idvariable in log messages.apps/connect/handlers/script.py(ScriptHandler): replacedsubprocess.runwith a_posix_runhelper that usesos.posix_spawn+ cooperativeselect.selectreads + non-blockingwaitpidpolling. 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:cwdis 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 viasubprocess.Popento verify per-plugin manifest signatures, triggering the samefork()atfork deadlock described above. Replaced with a_gpg_run()helper that usesos.posix_spawn, matching the pattern used by the ffmpeg and script-handler fixes.select.select()drains stdout/stderr cooperatively (gevent-patched) andos.waitpid(WNOHANG)withtime.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.http://server/Pluto/gb/player_api.php), Dispatcharr was stripping the path entirely and hitting the root (/player_api.php) instead._normalize_urlnow preserves sub-path components and only strips any trailing.phpsegment (coveringplayer_api.php,get.php,xmltv.php, and any future endpoint without a maintained list). The same fix is applied toget_transformed_credentialsin the M3U profile transformation path. (Fixes #1218)filter.type(alwaysundefined) instead offilter.filter_type, leaving the "Type:" line blank, and displayed "Patter:" instead of "Pattern:". Both are corrected. — Thanks @nick4810FileInputto expand beyond the modal's layout bounds. The input now clips overflow withtextOverflow: ellipsis. — Thanks @nick4810setIsLoading(false)was inside thecatchblock only, so a successful login that immediately navigated away left the loading state astrueif the component re-mounted. Moved to afinallyblock so it always resets. — Thanks @nick4810ChannelBatch.jsxonly wroteconsole.errorwhen 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 @CodeBormenRecurringRuleModalread 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 viaAPI.getChannelsSummary()when it opens, matching the lightweight approach used by the one-time recording form and other modals.Group justify="space-between", which pushed the badge to the opposite edge of the full-width container. Changed toGroup gap="xs" align="center"so each badge sits inline with its heading.trigger_eventinapps/connect/utils.pyiteratedpm.list_plugins()and, for disabled plugins, logged a debug message usingplugin.key/plugin.name(attribute access). Becauselist_plugins()returns dicts, this raisedAttributeErroron the first disabled plugin encountered. Fixed by changing the two accesses toplugin['key']/plugin['name']. (Fixes #1231) - Thanks @R3XCHRISINSTALLED_APPSsoautodiscover_tasks()never imports them, and worker startup skips plugin discovery viashould_skip_initialization(). Any plugin using module-level@shared_taskhad its tasks unregistered with the worker until a lazy event import warmed the module; beat fired on schedule but the worker rejected withReceived unregistered taskand advancedlast_run_atanyway, hiding the miss. Aworker_readyhook indispatcharr/celery.pynow eagerly callsPluginManager.discover_plugins(sync_db=False)on every worker boot so plugin tasks are registered before beat starts firing. (Fixes #1244) - Thanks @R3XCHRISlazy-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 blockeddiscover_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 useworker_ready) and management commands that don't serve requests.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'smax_connectionslimit under moderate concurrency.DATABASE_CONN_MAX_AGEis now0so 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._close_socketininput/manager.pywas closing the relay pipe read-end (self.socket) before killing the ffmpeg process. The stream OS thread blocks inselect()on that fd; on Linux, closing an fd from another thread while aselect()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), andstream_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 toselect()immediately.self.socketis closed afterward as cleanup only.stop_channelcalls on every channel shutdown.StreamGenerator._cleanuptriggered channel shutdown via two parallel paths:client_manager.remove_client()(which fireshandle_client_disconnecton the owning worker, the correct path) and_schedule_channel_shutdown_if_needed(which independently spawned a delayedstop_channelgreenlet). The duplicate call was suppressed by the_stopping_channelsguard but produced a redundant log entry and unnecessary greenlet on every shutdown._schedule_channel_shutdown_if_neededhas been removed;handle_client_disconnectis the sole shutdown trigger._close_socketduringproc.wait().self.transcode_processwas set toNoneat the end of theif 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, findself.transcode_processstill set, and attempt a second kill+close.self.transcode_process = Noneis now assigned immediately afterproc = self.transcode_processso concurrent callers seeNoneand skip the block.Date.now() - uptime * 1000on every render. Becauseuptimeis 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 emitsstarted_at(the raw Unix timestamp from Redis) alongsideuptime, matching what the detailed stats path already sent. The frontendgetStartDatehelper now accepts the stablestarted_attimestamp directly, so the displayed wall-clock time is fixed from the first poll and never changes.MemoizedTableRowwas introduced,handleShiftSelectcapturedlastClickedIdfrom its render-time closure. Because the memo comparator intentionally excludes callback function references, unselected rows retained the stale closure wherelastClickedId === null, so every shift+click from a previously unchecked row fell through to a plain toggle instead of selecting the range. AddedlastClickedIdRefandallRowIdsRefalongside the existingselectedTableIdsRef;handleShiftSelectnow reads from those refs so every row uses the current anchor ID and full ID list regardless of which render produced the closure.MemoizedTableRowappliedbackgroundColor: '#​163632'based onrow.getIsSelected()from TanStack Table's API. Becausestate.rowSelectionwas never wired intouseReactTable,row.getIsSelected()always returnedfalseand selected rows remained unstyled. Changed to use theisSelectedprop, which is correctly derived fromselectedTableIdsSetand already tracked by the memo comparator.blurevent listener inuseTableleaked on component unmount. TheuseEffectcleanup function calledwindow.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 namedhandleBlurconstant so setup and cleanup reference the same function.Performance
django-redis. The default Django cache backend wasLocMemCache, 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 nowdjango_redis.cache.RedisCache, backed by the same Redis instance used bychannels_redis, so a single cached EPG copy is shared across all workers.AutoSyncAdvancedandLogoFormare 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 inReact.lazy+Suspenseremoves them from the initial bundle and defers their parse/execute cost until first use. — Thanks @nick4810sync_auto_channels. Per-rowChannel.objects.create()and.save()calls were replaced withbulk_create()andbulk_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 singlebulk_updateat the end of the loop.ChannelStream.orderwrites were similarly consolidated into a singlebulk_update.used_numbersset. Choosing a free channel number was previously anO(N)DB query per stream. The new path seeds aset()of all reserved numbers (existing channels + override pins) once per run;_next_available_number()isO(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.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+1ChannelOverridelookups across the channel scan and lets the same query serve the join.bulk_createandbulk_updatebypasspost_save, so the post-loop step explicitly dispatchesparse_programs_for_tvg_id.delayonce per uniqueepg_data_idactually changed in the run, not per channel. Avoids fan-out where one EPG source change otherwise queued thousands of redundant parse tasks.sync_auto_channelsnow caches Logo and EPGData lookups in a per-run dict so repeated provider URLs / IDs across many streams do not produce duplicateLogo.objects.get_or_createorEPGData.objects.filtercalls.update_channels_with_override_routingpartitions 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_streamsresponse-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 calledreverse()andbuild_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 singlereverse()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. Theget_live_streamsAPI endpoint now returns aStreamingHttpResponsebacked 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)GET /api/channels/streams/filter-options/previously ranDISTINCTqueries 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 simpleDISTINCTaggregations, skipping the request mutation and filterset instantiation overhead.DISTINCT.ChannelViewSet.get_queryset()previously appended.distinct()unconditionally.DISTINCTis only needed when the query joins a one-to-many table - specifically whenchannel_profile_idoronly_stalefilters are active. The queryset now returns plain results in the common case, eliminating sort-and-deduplicate overhead on most channel list fetches.JsonResponsefor ID list and channel summary endpoints. Thestream_ids,channel_ids, and channelsummaryendpoints now usedjango.http.JsonResponseinstead of DRF'sResponse, 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.channel_countto eliminate N+1 inLogoSerializer.LogoViewSet.get_queryset()now annotates each row withCount('channels').LogoSerializer.get_channel_count()andget_is_used()read the annotation directly instead of issuing a separateCOUNT(*)per logo. Theused=trueandused=falselist filters use the annotation for their conditions, removing theDISTINCTthat was previously required.ChannelProfileSerializerreads prefetched memberships.ChannelProfileViewSet.get_queryset()now prefetches enabledChannelProfileMembershiprows intoenabled_memberships.ChannelProfileSerializer.get_channels()uses the prefetched set when available, eliminating one query per profile in any response that lists multiple profiles.isShiftKeyDownReact state fromuseTable. Setting it on everykeydown/keyupevent 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 bydocument.body.classListmanipulation, so the React state served no purpose.selectedChannelIdsreactive Zustand store subscription fromChannelsTable. Each checkbox click wrote to both localselectedTableIdsstate and the store viaonRowSelectionChange, causing two sequential re-renders ofChannelsTableper click. The two consumers of this subscription (deleteChannelandChannelBatchForm) now read fromtable.selectedTableIdsdirectly.rowSelectionuseMemo that built a TanStack row-selection map fromselectedTableIds. The map was placed intableInstancebutstate.rowSelectionwas never passed touseReactTable, so TanStack never consumed it. Eliminated a full page-row iteration on every selection change.Configuration
📅 Schedule: (in timezone America/New_York)
🚦 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.
This PR was generated by Mend Renovate. View the repository job log.