Skip to content

v2026.07.07 - #51

Merged
coreyeaston merged 87 commits into
mainfrom
develop
Jul 7, 2026
Merged

v2026.07.07#51
coreyeaston merged 87 commits into
mainfrom
develop

Conversation

@coreyeaston

Copy link
Copy Markdown
Contributor

Summary

Turns Speedarr into a multi media-server platform and clears all 50 open Dependabot alerts.
87 commits, 115 files.

Changes

Upgrade notes

  • Config and bandwidth-split migrations run automatically on first start
  • Emby/Jellyfin are experimental; SNMP users should re-test their connection after upgrading

…ependabot config

- cut-release.yml: manual workflow that tags main (CalVer, collision-checked),
  dispatches the release build (token-pushed tags don't fire on:push), and
  merges main back into develop automatically
- release.yml: accepts workflow_dispatch on tag refs and comments 'Fixed in vX'
  on every issue closed by the release's commits
- dependabot.yml: weekly grouped version-update PRs targeting develop
- docs/RELEASING.md: rewritten release process notes
The failsafe shutdown path split the configured shutdown total across clients
and applied it directly via set_speed_limits(), with no floor. A shutdown speed
of 0 (or a 0% client share) therefore sent 0 to clients, which qBittorrent,
NZBGet and Deluge all read as 'unlimited' -- the opposite of throttling. This
is the same 0=unlimited class fixed for the live path in #43, which scoped out
the shutdown path.

- Move HARD_MIN_MBPS to constants.py as the shared floor (decision_engine now
  imports it from there; live-throttle behavior unchanged).
- Floor every per-client value in _split_shutdown_speed to HARD_MIN_MBPS so a
  shutdown of 0 or a 0% share becomes a non-zero trickle, never unlimited.
- Add ge=0 validation to FailsafeConfig shutdown_download_speed/upload_speed.
- Clamp the frontend shutdown-speed inputs against negatives/NaN and note that
  0 means a minimum trickle.
- Add tests covering zero-total, zero-share, never-sends-0, and validation.
Replace global include_lan_streams flag with per-stream policy so each
Plex server can independently control whether its LAN streams count
toward bandwidth calculations. Missing policy defaults to exclude LAN.
Adds aggregate_per_server_bandwidth() helper that sums stream bitrate
per server_id, and populates BandwidthMetric.per_server with the
JSON-serialised result on each polling cycle.
Per-client bandwidth allocations were keyed by client type (or
client_id.split('_')[0]), so two clients of the same type (e.g. two
qBittorrent) collided on one key and were forced to an equal split.

Re-key the live decision-engine download and upload split branches and
the failsafe shutdown-speed split to look up percentages by the unique
client id directly. Remove the now-dead get_client_type /
get_normalized_percents / get_raw_upload_percent helpers.

Adds engine tests for two same-type clients splitting 60/40 (download)
and 70/30 (upload), a legacy id==type regression test, and a same-type
failsafe split test; re-keys the stale type-keyed shutdown fixture.
Re-key the stored per-client percent dicts (live + scheduled download/
upload and failsafe shutdown) from client type to unique client id:

- resolve_percent_key / normalize_percent_dict / normalize_client_percent_keys:
  pure in-memory normalizer applied on every config load (read-time safety
  net, no DB I/O). Rule order: id-exact keeps, single-type renames to that
  id, multi-type collision drops, dead key drops.
- migrate_client_percent_keys: one-time persisted migration run once at
  startup that deletes superseded type-keyed rows and inserts renamed
  id-keyed rows (idempotent). Persistence lives only at startup, never in
  the read-only load path.
- cleanup_bandwidth_percent_keys: clears live+scheduled percent rows before
  a bandwidth save so renamed/removed id keys can't be resurrected.
- settings _prune_percent_dicts: prune all six percent dicts to the live
  client-id set on client add/remove (replaces the old type-set reset).
- config.py field docs updated to say client id; no schema change.

Two user-visible behaviors on first upgrade:
- A config that previously had two colliding same-type clients resets to an
  equal split until a slider is moved.
- Per-client splits are pruned to the current client-id set on any client save.

Adds pure-helper, DB migration (rename/drop/dead-key/idempotent), cleanup,
and prune tests.
Re-key every per-client percent getter, updater, and binding in the
bandwidth settings, setup wizard, and failsafe settings to use the unique
client.id instead of client.type. This fixes the unmovable split slider
for two clients of the same type: the slider updater's
find(c => c.type !== clientType) returned undefined for a same-type pair
and bailed before writing; it now uses find(c => c.id !== clientId).

The dashboard chart is intentionally left keyed by type (its data series
are aggregated per type by the chart-data API), so same-type clients merge
into one series; a comment documents this and per-client-id chart series is
left as a follow-up.
Record bandwidth per client id in a new per_client JSON column and serve
per-id chart fields plus a client_series descriptor list, so the chart can
plot multiple same-type clients separately. Legacy per-type columns now hold
the sum across same-type clients (fixing a first-client-only undercount), and
pre-fix rows fall back to one merged series per type.
Drive every per-client chart element (gradients, areas, limit lines,
aggregation, stacking, legend, visibility) from an id-keyed series descriptor
list built from the status clients and chart-data client_series, with an
auto-distinct color fallback for same-type clients. Fixes two qBittorrent
clients merging into one mislabeled series; pre-fix history shows a merged
line that splits into per-id lines for new data.
The chart-data API returns both legacy type-keyed series (from pre-fix rows)
and the new per-id series, which rendered as duplicate legend entries (e.g. two
"qBittorrent Download"). Fold each legacy type series into the first current
client of that type so single-instance clients show one continuous line across
the storage cutover, and drop the now-redundant legacy entry from the legend.
Removed individual clients and fully-removed types are preserved.
Add the per_client JSON column (mirroring BandwidthMetric), record decisions per client id in the polling writer, and serve them per id with a legacy-column fallback for pre-migration rows. Also removes the orphaned Plex failsafe globals (superseded by per-server _server_state).
… override

Emby/Jellyfin /Sessions exposes no per-session local flag, so streams were
classified by a blanket RFC1918 check that ignored each server's configured
LAN subnets. Now Speedarr reads LocalNetworkSubnets from each Emby/Jellyfin
config API and classifies against it, with a manual per-server lan_networks
CIDR override for all server types; Plex keeps its per-session signals.

Shared IP/CIDR logic moves to app/utils/network.py (is_private_ip,
ip_in_networks, classify_lan), removing the duplicated _is_private_ip from the
Plex and Jellyfin adapters. Auto-read subnets refresh at startup, on
media-server settings save, and on a successful Test Connection — never in the
5s poll loop. Adds a per-server LAN networks override input in settings.
…rvers

Jellyfin/Emby leaked the embedded video track Title (the movie/episode name)
into the Quality column because quality_profile was set from
MediaStreams[0].DisplayTitle - the first stream is not guaranteed to be the
video stream, and DisplayTitle embeds the track Title.

Add a pure helper (app/utils/quality.py) that normalizes resolution tokens to a
single canonical vocabulary (4K, 1080p, 720p, 480p, SD, 8K). Jellyfin/Emby now
select the video stream by Type and extract the last resolution token from its
DisplayTitle; Plex normalizes its videoResolution through the same helper. All
three servers now emit a consistent, title-free Quality value.
…che aliasing

The WAN gateway quantizes its SNMP octet counters to a ~5s internal cache,
equal to the 5s poll interval. Two-point differencing of cumulative counters
then aliased: each poll window randomly caught 0, 1, or 2 device refresh
steps, producing 0.00 dropouts and 2x spikes (observed up to ~1900 Mbps) that
slipped past the 10 Gbps sanity gate. Those spurious values also fed the
decision engine and caused over-aggressive download throttling.

Replace the last-two-reads delta with a rate averaged over a 30s sliding
window (RATE_WINDOW_SECONDS). Counters are cumulative, so the windowed byte
delta is exact; only the window endpoints carry the device's <=5s refresh
phase error, which is negligible over 30s (~2% vs the previous +/-50%+).

Adds compute_windowed_rate() helper and unit tests covering aliasing
cancellation, counter wrap (32/64-bit), and insufficient-sample handling.
Poll Unraid's GraphQL API and throttle download clients while a parity
check, the mover, or a degraded array is active. Replaces the bash-script
integration with native monitoring.

- UnraidConfig schema + settings section + test-connection endpoint
- UnraidMonitor GraphQL client (parity / mover / degraded-array detection)
- PollingMonitor override slot + dedicated poll loop; combine with manual
  temporary limits by most-restrictive so 0 still floors to a trickle
- Hold last-known throttle when the Unraid API is unreachable
- Rebuild/disable the monitor on settings save
- Surface the override in GET /temporary-limits
- unraid_throttle_started / unraid_throttle_ended notification events
- New Unraid panel (enable, URL, API key, condition toggles, shared
  download/upload limits, test connection) under the Integrations tab
- Remove the parity-check / mover bash-script section from API Keys
- Add the Unraid throttle start/end events to the notification selector
…pt approach (#30)

The Unraid GraphQL API does not expose a live parity check on Unraid 7.3.1 /
API 4.34.0: array.parityCheckStatus returns the last COMPLETED check, vars.mdResync*
are cached/stale, mdResyncAction never clears on stop, and arraySubscription is
broken (returns null). The live parity signal only exists host-side (/proc/mdstat),
so the original script-based push integration is restored.

Reverts commits 1453ce3 and 42ffb16.
…e 3.5.0

Clears 44 Dependabot alerts (33 aiohttp, 9 python-multipart, 2 python-jose
including the critical CVE-2024-33663). No code changes required: aiohttp is
client-only here, python-multipart is an unused FastAPI soft dependency
(login is JSON), and JWT decode already pins algorithms=[HS256].
python-jose 3.5.0 (not 3.4.0) because 3.4.0 pins pyasn1<0.5.0.
Replaces the deprecated pysnmp-lextudio package (which pinned pyasn1<0.6.1
and blocked the fix for the pyasn1 unbounded-recursion DoS, CVE-2026-30922)
with the maintained pysnmp 7.1.x line. Code changes required by 7.x:
snake_case get_cmd/next_cmd imports, awaited UdpTransportTarget.create()
at all four call sites, engine.close_dispatcher(), and a flattened
_walk_oid loop since next_cmd now returns a flat tuple of ObjectType
instead of 6.x's list-of-rows. Adds walk regression tests pinning the
flat shape and drops the now-dead pysnmp-lextudio warning filter.

Supersedes PR #35 (which proposed pysnmp 6.2.6, an unmaintained
migration-helper release, and kept the vulnerable pyasn1 pin).
Credit to @NeoMod for flagging the deprecated dependency.
…ubtree

_walk_oid's boundary check compared raw OID strings, so walking
...31.1.1.1.1 (ifName) would not stop at sibling columns like
...31.1.1.1.15 (ifHighSpeed) whose number begins with the same digit.
On devices where the walk lands on such a column, the extra rows key by
the same last OID component as real interfaces and overwrite their names
in discovery. Compare with a trailing dot so only true sub-OIDs match.
vite 5.x is EOL and received no backports for its three open advisories
(dev-server-only exposure, but unfixable on the 5.x line). 6.4.3 is a
drop-in for this config: plugin-react ^4.2.1 already resolves to a
vite-6-compatible release and the node:20.11 build stage satisfies
vite 6's engine range. Also commits package-lock.json and switches the
Docker build to npm ci — image builds previously ran a bare npm install
with no lockfile, silently floating every frontend dependency to the
newest in-range version on each build.
The previous lockfile was generated with npm install --package-lock-only,
which omits platform-specific optionalDependencies (npm/cli#4828). npm ci
then installed rollup without its native binary and vite build failed on
CI's linux/amd64 leg with 'Cannot find module @rollup/rollup-linux-x64-musl'.
Regenerated via a full npm install so the lockfile records every platform
variant; verified with a linux/amd64 build of the frontend stage.
…nfig

Covers the previously-unwatched base images in backend/Dockerfile, holds
new releases for 7 days (30 for majors) before proposing them, and
documents that Dependabot only reads this file from the default branch
- the config is inert until it reaches main with the next release.
@coreyeaston
coreyeaston merged commit 0cd8344 into main Jul 7, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant