From c80492c60bbd534ed5ef085a261f75cf6641c94b Mon Sep 17 00:00:00 2001 From: Sportarr Date: Tue, 4 Aug 2026 11:23:13 -0400 Subject: [PATCH 1/2] Add Sportarr support Adds Sportarr (https://github.com/Sportarr/Sportarr), a sports event manager exposing a Sonarr-v3-compatible API, as a supported instance type. It rides the sonarr-shaped constants (api/v3, series/episode keys, EpisodeSearch/RefreshSeries commands, includeUnknownSeriesItems) and joins sonarr in every per-type conditional: instance loading, refresh plumbing, deletion watchers, search logging, and startup rejoin. README and config example gain SPORTARR/sportarr blocks. Full test suite passes (292) and ruff reports no new findings on the touched files. --- README.md | 6 +++++- config/config_example.yaml | 3 +++ src/deletion_handler/deletion_handler.py | 1 + src/jobs/search_handler.py | 2 +- src/settings/_constants.py | 7 +++++++ src/settings/_instances.py | 4 ++-- src/settings/_user_config.py | 2 +- src/utils/startup.py | 2 +- 8 files changed, 21 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b375f91a..e073727b 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Looking to **upgrade from V1 to V2**? Look [here](#upgrading-from-v1-to-v2) Decluttarr is a helper tool that works with the *arr-application suite, and automates the clean-up for their download queues, keeping them free of stalled / redundant downloads. -It supports [Radarr](https://github.com/Radarr/Radarr/), [Sonarr](https://github.com/Sonarr/Sonarr/), [Readarr](https://github.com/Readarr/Readarr/), [Lidarr](https://github.com/Lidarr/Lidarr/), and [Whisparr](https://github.com/Whisparr/Whisparr/). +It supports [Radarr](https://github.com/Radarr/Radarr/), [Sonarr](https://github.com/Sonarr/Sonarr/), [Sportarr](https://github.com/Sportarr/Sportarr/), [Readarr](https://github.com/Readarr/Readarr/), [Lidarr](https://github.com/Lidarr/Lidarr/), and [Whisparr](https://github.com/Whisparr/Whisparr/). Feature overview: @@ -276,6 +276,10 @@ services: - base_url: "http://sonarr2:8989" api_key: "$SONARR_API_KEY" + # SPORTARR: > + # - base_url: "http://sportarr:1867" + # api_key: "$SPORTARR_API_KEY" + # RADARR: > # - base_url: "http://radarr:7878" # api_key: "$RADARR_API_KEY" diff --git a/config/config_example.yaml b/config/config_example.yaml index 2bbf8094..01277f82 100644 --- a/config/config_example.yaml +++ b/config/config_example.yaml @@ -55,6 +55,9 @@ instances: - base_url: "http://sonarr:8989" api_key: "xxxx" # timeout: 30 # Optional: Overrides general timeout for this instance. + # sportarr: + # - base_url: "http://sportarr:1867" + # api_key: "xxxx" radarr: - base_url: "http://radarr:7878" api_key: "xxxx" diff --git a/src/deletion_handler/deletion_handler.py b/src/deletion_handler/deletion_handler.py index aa1511f3..8b180f67 100644 --- a/src/deletion_handler/deletion_handler.py +++ b/src/deletion_handler/deletion_handler.py @@ -122,6 +122,7 @@ async def get_folders_to_watch_for_arr(self, arr): folders_to_watch = [] if arr.arr_type not in ( "sonarr", + "sportarr", "radarr", ): # only working for sonarr / radarr for now return folders_to_watch diff --git a/src/jobs/search_handler.py b/src/jobs/search_handler.py index 671ab535..602f6393 100644 --- a/src/jobs/search_handler.py +++ b/src/jobs/search_handler.py @@ -122,7 +122,7 @@ async def _log_items(self, items): title = item.get("title", "Unknown") logger.verbose(f"- {title}") - elif self.arr.arr_type == "sonarr": + elif self.arr.arr_type in ("sonarr", "sportarr"): logger.debug( "search_handler.py/_log_items: Getting series information for better display in output" ) diff --git a/src/settings/_constants.py b/src/settings/_constants.py index 904560e0..ed8a60bc 100644 --- a/src/settings/_constants.py +++ b/src/settings/_constants.py @@ -22,6 +22,7 @@ class Paths: class ApiEndpoints: radarr = "/api/v3" sonarr = "/api/v3" + sportarr = "/api/v3" lidarr = "/api/v1" readarr = "/api/v1" whisparr = "/api/v3" @@ -31,6 +32,7 @@ class ApiEndpoints: class MinVersions: radarr = "5.10.3.9171" sonarr = "4.0.9.2332" + sportarr = "4.0.0.0" lidarr = "2.11.1.4621" readarr = "0.4.15.2787" whisparr = "2.0.0.548" @@ -41,6 +43,7 @@ class MinVersions: class FullQueueParameter: radarr = "includeUnknownMovieItems" sonarr = "includeUnknownSeriesItems" + sportarr = "includeUnknownSeriesItems" lidarr = "includeUnknownArtistItems" readarr = "includeUnknownAuthorItems" whisparr = "includeUnknownSeriesItems" @@ -49,6 +52,7 @@ class FullQueueParameter: class DetailItemKey: radarr = "movie" sonarr = "episode" + sportarr = "episode" lidarr = "album" readarr = "book" whisparr = "episode" @@ -57,6 +61,7 @@ class DetailItemKey: class DetailItemSearchCommand: radarr = "MoviesSearch" sonarr = "EpisodeSearch" + sportarr = "EpisodeSearch" lidarr = "AlbumSearch" readarr = "BookSearch" whisparr = None @@ -65,8 +70,10 @@ class DetailItemSearchCommand: class RefreshItemKey: radarr = "movie" sonarr = "series" + sportarr = "series" class RefreshItemCommand: radarr = "RefreshMovie" sonarr = "RefreshSeries" + sportarr = "RefreshSeries" diff --git a/src/settings/_instances.py b/src/settings/_instances.py index 99bd772b..ee682c1d 100644 --- a/src/settings/_instances.py +++ b/src/settings/_instances.py @@ -91,7 +91,7 @@ def config_as_yaml(self, *, hide_internal_attr=True): } outputs = [] - for arr_type in ["sonarr", "radarr", "readarr", "lidarr", "whisparr"]: + for arr_type in ["sonarr", "sportarr", "radarr", "readarr", "lidarr", "whisparr"]: arrs = self.get_by_arr_type(arr_type) if arrs: output = get_config_as_yaml( @@ -174,7 +174,7 @@ def __init__(self, settings, arr_type: str, base_url: str, api_key: str, timeout self.detail_item_id_key = self.detail_item_key + "Id" self.detail_item_ids_key = self.detail_item_key + "Ids" self.detail_item_search_command = getattr(DetailItemSearchCommand, arr_type) - if self.arr_type in ("radarr", "sonarr"): + if self.arr_type in ("radarr", "sonarr", "sportarr"): self.refresh_item_key = getattr(RefreshItemKey, arr_type) self.refresh_item_id_key = self.refresh_item_key + "Id" self.refresh_item_command = getattr(RefreshItemCommand, arr_type) diff --git a/src/settings/_user_config.py b/src/settings/_user_config.py index 93526acf..a1583b20 100644 --- a/src/settings/_user_config.py +++ b/src/settings/_user_config.py @@ -41,7 +41,7 @@ "SEARCH_UNMET_CUTOFF", "SEARCH_MISSING", ], - "instances": ["SONARR", "RADARR", "READARR", "LIDARR", "WHISPARR"], + "instances": ["SONARR", "SPORTARR", "RADARR", "READARR", "LIDARR", "WHISPARR"], "download_clients": ["QBITTORRENT"], } diff --git a/src/utils/startup.py b/src/utils/startup.py index 535ab331..e61eef85 100644 --- a/src/utils/startup.py +++ b/src/utils/startup.py @@ -133,7 +133,7 @@ async def retry_degraded_instances(settings, watch_manager=None): ) if ( watch_manager - and getattr(unit, "arr_type", None) in ("sonarr", "radarr") + and getattr(unit, "arr_type", None) in ("sonarr", "sportarr", "radarr") and settings.jobs.detect_deletions.enabled ): await watch_manager.setup_for_arr(unit) From 79851e6248da69a2f9388a2b6a1b70ff6c8ea00e Mon Sep 17 00:00:00 2001 From: Sportarr Date: Tue, 4 Aug 2026 14:50:03 -0400 Subject: [PATCH 2/2] Detect Sportarr via its identity marker instead of appName --- src/settings/_instances.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/settings/_instances.py b/src/settings/_instances.py index ee682c1d..6081b2c0 100644 --- a/src/settings/_instances.py +++ b/src/settings/_instances.py @@ -219,6 +219,13 @@ def _check_min_version(self, status): def _check_arr_type(self, status): """Check if the ARR instance is of the correct type.""" actual_arr_type = status["appName"] + if self.arr_type == "sportarr": + # Sportarr's Sonarr-compatible API reports appName "Sonarr" (other + # consumers validate on that), so its identity marker is the extra + # sportarrVersion field a real Sonarr never sends. + if "sportarrVersion" in status: + return + actual_arr_type = "Sonarr" if actual_arr_type.lower() != self.arr_type: logger.error("!! %s Error: !!", self.name) logger.error(