Skip to content

feat: v3 — multi-instance Radarr/Sonarr support, web UI redesign, setup wizard - #131

Merged
sbcrumb merged 68 commits into
devfrom
feature/v3
Oct 7, 2026
Merged

sbcrumb merged 68 commits into
devfrom
feature/v3

Conversation

@sbcrumb

@sbcrumb sbcrumb commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Summary

Promotes feature/v3 to dev — 67 commits of work developed and iterated on this branch over ~6 weeks, beta-tested live by two users (myself in production, plus a second real dual-instance setup).

What's in this

  • Multi-instance Radarr/Sonarr support — name multiple instances, per-instance paths/config, instance-aware webhook routing, DB rows scoped by (imdb_id, instance).
  • Web UI redesign — sidebar navigation, instance badges, per-instance filtering, delete-series, skipped-items card, chart fixes.
  • Setup wizard — guided .env/.env.secrets generation, per-instance connection testing, env backup/restore.
  • Readable logs + web-based log viewer — configurable LOG_LEVEL, per-webhook request IDs, compact webhook payload summaries, in-browser log tail/download (Tools tab).
  • Plugin lookup visibility — titled, reasoned log lines for Emby/Jellyfin plugin lookups; new "Unresolved Lookups" page persisting "not found" results instead of only logging them.
  • Series IMDb-ID auto-migration — detects Sonarr re-mapping a series' IMDb ID (common for brand-new titles) and merges episode history automatically instead of silently forking it into a duplicate row.
  • Populate Database: added/skipped item lists surfaced in the UI; cold-start fixes; per-instance file-info scoping.
  • Docs, .env.example, and docker-compose.yml.example updated throughout; no internal IPs/hostnames.

Testing

  • No automated test suite exists in this repo (pre-merge check reduces to "confirm the app starts" per CLAUDE.md).
  • Verified via docker-compose.local-test.yml — clean startup, no traceback, health endpoint responds.
  • Live beta: running in production on my own dual-instance setup; a second user (burntslurm) independently running it on their own setup.

Not included here

  • NFO-layer removal — deliberately deferred to a follow-up branch after this reaches main (dead code, not user-facing, kept separate per the repo's cleanup-pacing convention).
  • License-server side of plugin-registration tracking — separate repo (chronarr-license-server), independent timeline.

Next steps after this merges

dev → main with a VERSION bump to 2.1.0, once smoke-tested again at dev's new tip.

sbcrumb and others added 30 commits August 25, 2026 07:55
Introduces RadarrInstance and SonarrInstance dataclasses and env var
discovery. RADARR_URL remains the default instance; RADARR_4K_URL,
RADARR_REMUX_URL etc. register additional instances automatically.
Each instance gets its own webhook path derived from the name segment
(RADARR_4K_URL -> /radarr_4k/webhook).

Backward compatible — single-instance setups need no config changes.
Introduces InstanceRegistry — built at startup from config.radarr_instances
and config.sonarr_instances. Tries DB client first per instance, falls back
to API client. Lookup by instance name returns the right client for that
instance. Missing name returns None rather than erroring.
Each movie and series row now carries an instance name derived from the
webhook URL (e.g. 'radarr', 'radarr_4k'). Primary keys are updated to
composite (imdb_id, instance) so the same title managed by two separate
Radarr instances stores as two independent records without collision.

Migration block handles existing installations: drops old FK before
altering series, rebuilds all three PKs, then re-adds the FK from
episodes to series on the new composite key. Existing records default
to 'radarr' / 'sonarr'.

All upsert, read, delete, and migrate methods updated with an instance
parameter (defaults preserve backward compat for single-instance users).
get_all_movie_records and get_all_series_records now include instance
in the result set for use by library sync.
PathMapper now takes explicit root_folders and container_paths in its
constructor — one per configured instance, built at startup rather than
re-reading env vars on every call. Longest-prefix match avoids collisions
when multiple root folders share a path prefix.

InstanceRegistry builds and stores one PathMapper per Radarr and Sonarr
instance alongside the existing clients. radarr_mapper(name) /
sonarr_mapper(name) look up the right one for a given webhook.

file_utils: replace the hasattr duck-typing for Radarr vs. Sonarr
method names with a single path_mapper.map() call. The old method
names remain as aliases on PathMapper for any other call sites.
Dynamic webhook routes are now registered at startup from the
InstanceRegistry. Each configured Radarr/Sonarr instance gets its own
route (e.g. /radarr_4k/webhook, /sonarr_anime/webhook). The legacy
/webhook/radarr and /webhook/sonarr paths stay as backward-compat
aliases for the default instances.

The instance name flows from the URL into the webhook handler, into
the batcher's webhook data dict, and into the processor's process_movie
/ process_series / process_webhook_episodes calls, which pass it to
every DB write — completing the path from URL to instance column.

MovieProcessor and TVProcessor now accept an optional pre-built client
from InstanceRegistry and fall back to the env-var construction for
single-instance setups. main.py builds the registry at startup and
wires the default instance's client into each processor.

should_skip_movie refactored from raw SQL to self.db.get_movie_dates
(removes the dead SQLite branch). should_skip_series_fast and
should_skip_series share a new _episode_completion_counts helper that
filters by instance.
…stry-based connection status

process_season, process_episode, async_process_series, and async_batch_episode_processing
now accept instance param (default sonarr) and pass it through all DB writes.

test_database_connections refactored to use the registry built at startup — iterates
over configured instances instead of reading env vars directly. initialize_components
accepts an optional registry so main() can build it once and share it.
InstanceRegistry now records connection status for every instance at build
time — {connected, method} for direct_db or api clients, {connected: False}
when no client is available.

radarr_status(name) / sonarr_status(name) expose per-instance status; the
bulk variants all_radarr_statuses() / all_sonarr_statuses() return all of
them. The /setup page will use these to display instance health without
re-testing connections.

test_database_connections() now reads from the registry status instead of
doing isinstance checks, and emits a plain-English summary warning when any
instance has no working client. Unused RadarrDbClient / SonarrDbClient
imports in main.py removed.
Adds a /setup page to the web interface showing every configured Radarr
and Sonarr instance with its webhook URL and connection status. Users can
copy the full URL directly from the page.

New core endpoint GET /api/instances returns instance names, webhook paths
(/radarr/webhook, /radarr_4k/webhook, etc.), connected flag, and client
method (direct_db / api). Reads from the registry built at startup.

Web container proxies /api/instances to the core and serves /setup at
static/setup.html. The page pre-fills the base URL from the browser's
current hostname (swapping port to 8080) so the user gets a usable URL
without any manual input. All webhook URLs update live when the base URL
field is edited.
Movies and TV series views now support instance filtering:

Backend:
- /api/movies and /api/series accept ?instance= query param; results are
  filtered to that instance when provided
- instance column included in movie/series SELECT so it appears in responses
- Series JOIN fixed: LEFT JOIN episodes e ON ... AND s.instance = e.instance
  ensures episode counts are per-instance in multi-instance setups
- .env.example notes /setup page for webhook URL lookup

Frontend:
- Instance dropdown added to Movies and TV filters; populated from
  /api/instances (proxied from core); hidden for single-instance setups
- Instance column added to both tables (shown only when >1 instance
  configured)
- Setup link added to nav bar
Emby-DLL/CHANGELOG.md contains the full Emby plugin version history.
The GET /emby/changelog endpoint on the core API serves this file as
markdown/plain text so the plugin config page can link to it directly
using the webhook URL the user has already configured.
feat: v3 Phase 3 — multi-instance UI, setup page, Emby changelog endpoint
…sode edits now scope correctly to the specific instance
…stances

DatabasePopulator gains from_radarr_instance() and from_sonarr_instance()
class methods that build the right DB or API client from a discovered
instance config, bypassing the env-var-only from_env() path.

populate_movies() and populate_tv_episodes() now accept an instance param
and pass it through to every upsert_*, get_*, and mark_* DB call so
records are tagged with the correct instance name.

_populate_worker_process loops over config.radarr_instances and
config.sonarr_instances rather than reading only RADARR_URL/SONARR_URL.
Stats are merged across all instances before writing the status file.
- README: add multi-instance support to features, sidebar nav to web
  interface section, multi-instance env var pattern to configuration,
  instance-specific webhook URLs, DELETE /api/series endpoint, v3.0.0
  changelog entry, Jellyfin plugin in privacy section
- CHANGELOG: add core app section with v3.0.0 entry
- QUICKSTART: update populate step to sidebar Tools (was Admin tab)
- correct webhook URLs: /webhook/radarr → /radarr/webhook (both files)
- remove reference to docker-compose.dev.yml which does not exist
- replace macOS-only 'open' command with plain prose
- update troubleshooting to say 'Movies or TV Shows sections' not 'tabs'
- add static/ to project structure listing
- .env.example: replace internal IP 192.168.255.50 with placeholder
  hostnames in all four instance blocks (radarr, sonarr, radarr_4k,
  sonarr_4k)
- .env.secrets.example: remove stale NFO/v0.6.0 version references,
  add missing SONARR_DB_PASSWORD, add commented named-instance
  password and API key slots, clean up TVDB comment
- docker-compose.yml.example: fix 'Admin → Populate Database' to
  sidebar Tools
- README: add v2 to v3 upgrade section covering webhook aliases,
  database migration, and multi-instance opt-in
start_web.py serves from chronarr-web/static/ not root static/ —
the sidebar nav changes were only applied to root static/ so the
web container was still showing the old interface
fix: sync sidebar nav static files to chronarr-web/static
- add Skipped Items stat card (was present in old UI, missing in v3)
  wired to movies_skipped/episodes_skipped from dashboard API
- fix .chart-container CSS: simple-chart items were being centered
  inside a fixed-height flex box, appearing clipped to one side;
  add .simple-chart/.chart-item/.chart-label/.chart-value styles so
  the source list fills the card properly
- sync changes to both static/ and chronarr-web/static/
JSON.stringify(title) produces double-quoted strings which break
the onclick="..." HTML attribute — the browser ends the attribute
value at the first quote character in the title, leaving truncated
JS that throws SyntaxError: Unexpected end of input at char 37.

Replace " with " in the rendered onclick so the HTML parser
round-trips the string correctly before the JS executes.
Each Radarr/Sonarr instance now defines its own paths variable
(RADARR_{NAME}_MOVIE_PATHS, SONARR_{NAME}_TV_PATHS). The global
MOVIE_PATHS and TV_PATHS are no longer required at startup; they
remain as optional fallbacks for single-instance setups.

When multiple instances share a global key in Docker env_file,
last-definition-wins silently drops all earlier values. Per-instance
vars are scoped to their block, so no instance can accidentally
overwrite another's paths.

- settings.py: _load_paths() no longer raises on missing global vars;
  adds _warn_missing_instance_paths() to log clearly at startup if
  any configured instance has no paths at all
- .env.example: MOVIE_PATHS → RADARR_MOVIE_PATHS / TV_PATHS →
  SONARR_TV_PATHS; prominent warning in multi-instance blocks
- docker-compose.yml.example: per-instance SQLite volume mount
  examples for all instance types
- README.md: multi-instance config section rewritten to show
  complete per-instance blocks with paths and DB vars together
fix: per-instance paths — remove global MOVIE_PATHS/TV_PATHS requirement
sqlite3.connect() silently creates an empty file at the given path if it
doesn't exist, so a broken or misconfigured volume mount previously looked
like a successful connection to an empty database instead of failing.
RadarrDbClient/SonarrDbClient now check the path exists before connecting.

DatabasePopulator.from_radarr_instance/from_sonarr_instance no longer catch
that failure and fall back to the API client when db_type is explicitly
configured — they let it raise, since silent fallback degraded every date
to a *_fallback source with no indication the DB access was broken. Only
instances with no db_type at all use the API client.

The per-instance populate worker now catches this per instance so one
misconfigured instance fails loudly without aborting population for the
other configured instances.
sbcrumb and others added 27 commits August 31, 2026 14:39
fix: Emby plugin date lookup and cross-instance path-scan isolation
Same root cause as the episode delete fix: DELETE /api/movies/{imdb_id}
and GET /api/debug/movie/{imdb_id}/raw called db.delete_movie()/
get_movie_dates() with no instance, silently defaulting to the radarr
instance. Deleting or debugging a movie that lives under a named
instance (e.g. radarr_strm) 404'd, because the lookup was always
scoped to the default instance's row instead.

Both endpoints now take an instance param; the Movies Database table's
Delete and Debug Data buttons thread the row's own instance through,
matching the Edit button which already did.
…scope

fix: movie delete and debug-data lookup ignored instance
Network/FUSE-backed mounts (e.g. decypharr's DFS backend for debrid/
usenet libraries) can report a just-created file as briefly unreadable
(ENOTCONN) for a few seconds right after it's symlinked in, while the
mount's own backing downloader for that file finishes initializing.
That's a timing race in the mount layer, not a real missing-file
condition, but it was failing the whole webhook outright on the first
hit - confirmed live against a real decypharr mount (a movie import
failed with [Errno 107] Transport endpoint is not connected seconds
after decypharr's own log said the file was ready).

Added list_video_files_with_retry() to utils/file_utils.py: retries
the whole directory listing + stat pass on OSError instead of letting
it propagate immediately. Starting point: 5 attempts, 3s apart (~15s
of patience before giving up, on top of the existing 5s batch delay).
Re-raises the last error once retries are exhausted, so a genuinely
broken mount still fails loudly rather than being silently treated as
"no video files found".

Applied at both places that scan a directory for video files as part
of webhook processing: MovieProcessor.process_movie()'s has_video
check and TVProcessor.process_webhook_episodes()'s episode-file
matching. Verified with a mocked Path.iterdir(): recovers after
transient failures within the retry budget, and still raises once
persistent failures exhaust it.
fix: retry on transient filesystem errors during webhook processing
…n the web UI

Readability:
- LOG_LEVEL is now configurable (default INFO, was hardcoded DEBUG) -
  applies to both the file logger and the console print() that
  docker compose logs actually shows, which wasn't gated by level at
  all before. Set LOG_LEVEL=DEBUG to get the old verbosity back.
- The full Radarr webhook payload (a multi-KB JSON blob) is no longer
  dumped at DEBUG by default - replaced with a compact one-line
  summary (title, imdb, year, path). Set LOG_RAW_WEBHOOK_PAYLOADS=true
  to get the old full-dump behavior back for deep-debugging a specific
  webhook.
- Every webhook now gets a short request id (6 hex chars) at receipt,
  tagged alongside the instance name on every log line through the
  synchronous receive/validate path in api/routes.py and the
  batch/process boundaries in webhooks/webhook_batcher.py (e.g.
  [radarr_strm:a3f9c1]) - makes it possible to tell which log lines
  belong to the same webhook when several land close together. Not
  threaded into the processors' own internal methods (would roughly
  double tonight's diff for one line of tagging depth); the instance
  tag alone still applies there.

Web UI log viewer (Tools tab):
- GET /api/logs lists the current log file and its rotated backups
  (size, modified time).
- GET /api/logs/{filename}/tail?lines=N returns the last N lines as
  JSON, for an in-browser tail view with manual refresh and an
  optional 5s auto-refresh.
- GET /api/logs/{filename}/download serves the raw file.
- All three live on chronarr-core (the only container with LOG_DIR
  mounted) and are proxied through chronarr-web the same way the
  wizard's env-backup download already is. Every filename is checked
  against a whitelist regex before touching the filesystem.

Verified: LOG_LEVEL gating (DEBUG suppressed by default, shown with
LOG_LEVEL=DEBUG), the log-file regex accepts only chronarr.log(.N)?
and rejects path traversal attempts, tail-from-file logic returns the
correct last N lines, and all three endpoints work end-to-end
(list/tail/download) including a live 404 on a path-traversal or
non-whitelisted filename attempt.
The Tools tab crammed five unrelated cards (bulk source update, manual
scan, database stats, populate database, log viewer) into one grid,
so a five-field form and a full log tail view were fighting each
other for space - the log viewer in particular was unreadable.

Tools is now a sidebar dropdown group, same pattern as Movies/TV Series,
with each tool getting its own full tab-content page:
- tool-bulk-source, tool-manual-scan, tool-stats, tool-populate, tool-logs
- New selectToolsPage() mirrors selectMovieInstance()/selectTvInstance()
  for sidebar active-state handling
- switchTab()'s per-tab load dispatch and the log-tail auto-refresh
  guard updated for the new tab ids (was gated on the single 'tools' id)
- Log viewer page gets a taller tail view (65vh) now that it isn't
  sharing a grid cell with four other cards

Old .tools-grid/.tool-card CSS replaced with .tool-page (single-column,
capped width for the form-only tools, uncapped via .tool-page-wide for
the log viewer). static/ synced to match chronarr-web/static/ per the
existing dual-copy requirement.

Frontend-only change, no Python touched. Verified: node --check on
app.js, and every sub-tool-* sidebar id has a matching tool-* tab-content
id. Not yet checked in a live browser - do that before merging.
feat: split Tools into its own sidebar dropdown, one page per tool
refreshLogTail() unconditionally did output.scrollTop = output.scrollHeight
on every refresh, including the 5s auto-refresh timer. Scrolling up to
read older lines got undone every single cycle - there was no way to
actually read anything with auto-refresh on.

Now only auto-scrolls to the bottom if the viewer was already there (or
within 24px of it) before the refresh. If you've scrolled up, a refresh
updates the content but leaves your scroll position alone.

Verified: node --check on app.js. Same live-browser caveat as the rest
of this branch - worth confirming by hand before merging.
fix: log tail auto-refresh yanked the viewer back to bottom every cycle
Populate Database was running entirely inside chronarr-web, which has
no LOG_DIR volume mount - every _log() call DatabasePopulator made
went to a path invisible on the host and lost on restart. docker
compose logs showed it live (print() doesn't care about mounts), but
the persisted chronarr.log never got it. Moved the real
implementation (_populate_worker_process, populate_database,
get_populate_status, multi-instance handling, status file) into
api/routes.py (chronarr-core, which has the mount); chronarr-web's
populate endpoints are now thin proxies, same pattern /manual/scan
already used.

Emby/Jellyfin plugin lookups (/api/lookup/episode, /api/lookup/movie
- same code path for a scheduled full-library scan and a single
real-time item add) only logged at DEBUG or on error. Since LOG_LEVEL
now defaults to INFO, a normal lookup produced zero lines on
Chronarr's side - only the plugin's own Emby/Jellyfin-side log showed
it happened. Added one INFO line per lookup (found/not-found +
source), matching the convention webhooks already use.

Verified: both files parse and import cleanly, function signatures
match their call sites, no dangling references to removed globals.
Not yet tested against a real running container - needs an actual
populate run and a real plugin scan to confirm chronarr.log gets
the lines this time.
fix: two log-visibility gaps found while testing the log viewer
Emby/Jellyfin plugin lookups only ever logged an IMDb ID, and a
not-found line gave no way to tell whether Chronarr has never seen
that title at all or has a row for it that just hasn't resolved a
date yet - one of those two is a real thing to go check, the other
is more likely a date-pipeline bug worth investigating.

- lookup_movie/lookup_episode now log a title when the DB has one
  (movies.title, or series.metadata['title'] for episodes via a new
  get_series_title_any_instance helper), and tag not-found lines with
  reason=no_db_record vs reason=no_resolved_date.
- Every not-found lookup is also persisted to a new unresolved_lookups
  table (upserted on imdb_id/media_type/season/episode, bumping
  miss_count/last_seen on repeat misses) instead of only existing as a
  log line that scrolls past during a full-library scan. A later
  successful lookup for the same item clears its row automatically.
- New GET /api/unresolved-lookups and POST
  /api/unresolved-lookups/{id}/dismiss endpoints (core), proxied
  through chronarr-web same as the existing log endpoints.
- New Tools > Unresolved Lookups page: IMDb-linked table of misses
  with reason/instance/miss count/last seen and a per-row Dismiss
  (snoozes a row; it resurfaces if the same item misses again later).
feat: title and reason on plugin lookup misses, unresolved lookups page
get_series_title_any_instance() read series.metadata['title'] on the
assumption TVProcessor's sync code sets it - it doesn't, every
upsert_series() call site omits the metadata kwarg, so the column is
always NULL and every episode lookup's title came back empty,
including on 'found'. Real production logs from PR #126 confirmed
this live.

Derive the title from the series directory name instead (path is
always set), using the existing extract_title_from_directory_name()
helper - the same '<Title> [imdb-ttXXXXXXX]' convention the directory
scan in find_media_path_by_imdb_and_title() already relies on.
…oads

#126 shipped loadUnresolvedLookups() and the new switchTab() case for
it, but never bumped the ?v= on the app.js <script> tag. Browsers (and
any edge cache in front of the stack) kept serving the pre-#126
cached bundle, which has neither the function nor the case - so
opening Tools > Unresolved Lookups just sat on the static 'Loading...'
placeholder from the HTML forever, with no network request ever firing
and nothing to see in chronarr's own logs because the request never
reached it.
fix: series title fix that missed #126, and stale-cached app.js
…lk dismiss

- populate_movies/populate_tv_episodes now track added_items alongside
  the existing skipped_items (same shape/convention), merged through
  the multi-instance worker and logged the same way (first 20 + '...and
  N more'). Surfaced in the Populate Database results panel as
  collapsed <details> lists for both Added and Skipped - previously
  only counts were shown even though skipped_items was already being
  collected and thrown away.
- The 'exists but updating file info' log lines (movies and episodes)
  now include the title - they only ever printed the raw file path
  before.
- lookup_movie's title now falls back to parsing the movie's directory
  name (same '[imdb-ttXXXXXXX]' convention as the series-title fix)
  when movies.title is NULL - a row created by the bare upsert_movie()
  path (still waiting on a date) never gets a title until the
  date-resolution pipeline runs, so 'in DB, no date yet' misses were
  showing as unknown even though a usable title was sitting right
  there in the path.
- Unresolved Lookups: added a Dismiss All button (dismiss_all_unresolved_lookups()
  + POST /api/unresolved-lookups/dismiss-all, proxied same as the
  per-row dismiss) - dismissing a long list one row at a time wasn't
  practical.
Real root cause of the flood of 'not found' movie lookups: libraries
checked off in Library Exclusions (e.g. a 4K/VOD library) were being
scanned anyway. The config page saves a library's ID in the dashed
GUID format Emby's Library/VirtualFolders API returns; the real-time
sync and scheduled task compare against ILibraryManager's own IDs
formatted with no dashes. Those never matched, so every exclusion
checkbox was silently a no-op regardless of which library it was.

Fixed in PluginConfiguration.GetExcludedLibraryIdSet() by normalizing
every stored ID through Guid.TryParse/ToString('N') before the
comparison set is built, so it no longer matters which format either
side hands us.

DLL built from chronarr-emby-plugin @ fix/library-exclusion-guid-mismatch,
version bumped 2.0.20 -> 2.0.21 (all 4 locations), SHA256 verified
matching the built artifact.
feat: populate added/skipped lists, movie title fallback, Emby plugin 2.0.21 exclusion fix
Sonarr can re-map a series' IMDb ID after the fact - common for very
new/upcoming titles whose metadata hasn't settled on a canonical ID
yet. series' PK is (imdb_id, instance), so a changed ID just created a
second row for the same show on the next sync, silently forking its
episode history across two entries (user-reported, reproduced: same
show, same path, two imdb_ids, 2 episodes under one and 1 under the
other).

upsert_series() now checks first whether a series already exists at
this (instance, path) under a different imdb_id - the path is the
stable signal here, it doesn't change just because a metadata ID got
corrected - and if so merges the two the same way the existing manual
migrate_series_imdb_id() does: episodes move to the new ID, any
season/episode that collides between the two old/new rows (a genuine
clash, not just happenstance) is dropped rather than crashing on the
primary key, and the stale series row is removed.

Self-heals existing duplicates on their next sync, no manual action
needed. Scoped to TV only for now - movies have the same underlying
risk (Radarr can remap an IMDb ID too) but two separate write paths
with different signatures, only one of which has a path to match on;
left as a deliberate follow-up rather than folding it in here.
The DLL merged in PR #128 was the first, incomplete v2.0.21 build
(GUID dash-format normalization only, SHA256 bf6bca18...) - real root
cause turned out to be config.js capturing the wrong VirtualFolderInfo
field (Id, a legacy numeric string on some Emby versions, not ItemId
or the actual Guid field). Fixed and confirmed live against a real
server (excluded library -> excluded=True, non-excluded ->
excluded=False) at v2.0.22, SHA256 de996550... - see
chronarr-plans/v3-plan.md Part 7 for the full story. Swapping in the
DLL that actually works.
…-emby-dll

feat: auto-migrate series IMDb ID changes + verified Emby plugin 2.0.22 DLL
PR #129 bundled 2.0.22 before the NFOG-/CHRO- license key prefix fix
was found and added on top of it (same DLL version, no bump needed -
pure backend logic). This is the DLL the user has confirmed working
live for both the library-exclusions fix and the license fix.

SHA256 e0b5830c... (was de996550...).
fix: update bundled Emby plugin DLL with license checksum fix
# Conflicts:
#	Emby-DLL/Chronarr.Emby.Plugin.dll
@sbcrumb
sbcrumb merged commit a6ebf08 into dev Oct 7, 2026
4 checks passed
@sbcrumb
sbcrumb deleted the feature/v3 branch October 7, 2026 16:08
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