From a41eccbe3c29adced7f5e65bb2a9df1bf30c2d8d Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Fri, 22 May 2026 08:28:56 -0700 Subject: [PATCH] Fix Usenet settings reload without restart Refresh registry-backed download plugins when settings are saved so cached Prowlarr clients pick up new indexer credentials immediately. This preserves active download state by reloading existing plugin instances instead of rebuilding the registry. Add regression coverage for orchestrator reload fanout and the Usenet plugin's cached ProwlarrClient refresh path. --- core/download_orchestrator.py | 16 ++++++++++ tests/downloads/test_download_orchestrator.py | 31 +++++++++++++++++++ tests/test_torrent_usenet_plugins.py | 28 +++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/core/download_orchestrator.py b/core/download_orchestrator.py index 879d0f52..1984c0a6 100644 --- a/core/download_orchestrator.py +++ b/core/download_orchestrator.py @@ -113,6 +113,22 @@ class DownloadOrchestrator: if hasattr(amazon, '_client') and amazon._client: amazon._client.preferred_codec = quality + # Let registry-backed plugins refresh any config they cache at + # construction time. This covers Prowlarr-backed torrent / usenet + # clients without rebuilding the registry and losing active downloads. + for name, client in self.registry.all_plugins(): + if not hasattr(client, 'reload_settings'): + continue + try: + client.reload_settings() + logger.info("%s client settings reloaded", self.registry.display_name(name)) + except Exception as exc: + logger.warning( + "%s client settings reload failed: %s", + self.registry.display_name(name), + exc, + ) + # Reload download path for all clients that cache it. # Soulseek owns the path config and is reloaded above; every # other source mirrors that path so files all land in one diff --git a/tests/downloads/test_download_orchestrator.py b/tests/downloads/test_download_orchestrator.py index e157047e..b551750e 100644 --- a/tests/downloads/test_download_orchestrator.py +++ b/tests/downloads/test_download_orchestrator.py @@ -198,6 +198,37 @@ def test_reload_instances_with_no_args_reloads_every_source(): assert b.reload_called is True +def test_reload_settings_refreshes_registry_plugins(monkeypatch): + """Settings saves should refresh plugins that cache config at init. + + Prowlarr-backed torrent / usenet clients keep a ProwlarrClient + instance, so without this hook newly-saved indexer settings only + took effect after process restart. + """ + + class _ReloadSettingsClient(_FakeClient): + def __init__(self): + super().__init__() + self.reload_calls = 0 + + def reload_settings(self): + self.reload_calls += 1 + + torrent = _ReloadSettingsClient() + usenet = _ReloadSettingsClient() + orch = _build_orchestrator(torrent=torrent, usenet=usenet) + + monkeypatch.setattr( + 'core.download_orchestrator.config_manager.get', + lambda _key, default=None: default, + ) + + orch.reload_settings() + + assert torrent.reload_calls == 1 + assert usenet.reload_calls == 1 + + # --------------------------------------------------------------------------- # Singleton factory (matches Cin's get_metadata_engine pattern) # --------------------------------------------------------------------------- diff --git a/tests/test_torrent_usenet_plugins.py b/tests/test_torrent_usenet_plugins.py index 57f11079..1dd9f75b 100644 --- a/tests/test_torrent_usenet_plugins.py +++ b/tests/test_torrent_usenet_plugins.py @@ -381,6 +381,34 @@ def test_usenet_is_configured_requires_both_sides() -> None: # --------------------------------------------------------------------------- +def test_usenet_reload_settings_refreshes_cached_prowlarr_config(monkeypatch) -> None: + """Settings saves must update the plugin's held ProwlarrClient. + + The active usenet adapter is rebuilt from config on each call, but + ProwlarrClient is cached inside the plugin. This is the path that + used to require a process restart after entering Prowlarr settings. + """ + settings = { + 'prowlarr.url': '', + 'prowlarr.api_key': '', + } + monkeypatch.setattr( + 'core.prowlarr_client.config_manager.get', + lambda key, default=None: settings.get(key, default), + ) + + plugin = UsenetDownloadPlugin() + assert plugin._prowlarr.is_configured() is False + + settings.update({ + 'prowlarr.url': 'http://prowlarr:9696', + 'prowlarr.api_key': 'secret', + }) + plugin.reload_settings() + + assert plugin._prowlarr.is_configured() is True + + def test_plugins_conform_to_protocol() -> None: from core.download_plugins.base import DownloadSourcePlugin assert isinstance(TorrentDownloadPlugin(), DownloadSourcePlugin)