From 72562eab95d95427081d43d9449a81acf8005ead Mon Sep 17 00:00:00 2001 From: Daniel Gradman-Svendsen Date: Wed, 29 Jul 2026 21:37:20 +0200 Subject: [PATCH] Target manual importer from UI controls #31 --- README.md | 2 +- importarr/config.py | 8 ++++-- importarr/main.py | 5 ++-- tests/test_queue_controls.py | 51 ++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7fc648c..7ffe7c5 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ sudo -n sh /opt/importarr/repo-upgrade.sh The helper refuses to run when the checkout has uncommitted changes, then performs `git pull --ff-only`, reinstalls the package from the repo, restarts `importarr.service`, and prints service status. Use it after changes have been committed and pushed to `main`. -Installed deployments can expose the same operation through the authenticated API. `GET /api/control/update-check` queries the latest release from `IMPORTARR_UPDATE_RELEASE_URL` (default: this repository's Gitea latest-release API) and compares it with the running `IMPORTARR_VERSION`. `POST /api/control/update` performs the same check and only runs the update command when a newer release tag exists. Configure `IMPORTARR_UPDATE_COMMAND` when the default `sh deploy/repo-upgrade.sh` is not correct for the service working directory, and configure `IMPORTARR_RESTART_COMMAND` when the default `systemctl restart importarr.service` needs a wrapper such as sudo. +Installed deployments can expose the same operation through the authenticated API. `GET /api/control/update-check` queries the latest release from `IMPORTARR_UPDATE_RELEASE_URL` (default: this repository's Gitea latest-release API) and compares it with the running `IMPORTARR_VERSION`. `POST /api/control/update` performs the same check and only runs the update command when a newer release tag exists. Configure `IMPORTARR_UPDATE_COMMAND` when the default `sh deploy/repo-upgrade.sh` is not correct for the service working directory. The web UI Start, Stop, and Restart controls target `manual-media-import.service` by default; configure `IMPORTARR_START_COMMAND`, `IMPORTARR_STOP_COMMAND`, or `IMPORTARR_RESTART_COMMAND` when those defaults need a wrapper such as sudo. Release-worthy changes should be committed, tagged with SemVer (`v0.1.1`, `v0.2.0`, ...), pushed with tags, then installed from the tagged checkout or artifact. diff --git a/importarr/config.py b/importarr/config.py index 569f25f..95115e4 100644 --- a/importarr/config.py +++ b/importarr/config.py @@ -22,7 +22,9 @@ class Settings(BaseModel): sonarr_url: str | None = None sonarr_api_key: str | None = None auth_token: str | None = None - restart_command: list[str] = Field(default_factory=lambda: ["systemctl", "restart", "importarr.service"]) + start_command: list[str] = Field(default_factory=lambda: ["systemctl", "start", "manual-media-import.service"]) + stop_command: list[str] = Field(default_factory=lambda: ["systemctl", "stop", "manual-media-import.service"]) + restart_command: list[str] = Field(default_factory=lambda: ["systemctl", "restart", "manual-media-import.service"]) update_command: list[str] = Field(default_factory=lambda: ["sh", "deploy/repo-upgrade.sh"]) update_release_url: str = "https://gitea.delphas.dk/api/v1/repos/daniels/importarr/releases/latest" update_check_timeout_seconds: int = Field(default=15, ge=1) @@ -48,7 +50,9 @@ class Settings(BaseModel): sonarr_url=os.getenv("IMPORTARR_SONARR_URL"), sonarr_api_key=_env_secret("IMPORTARR_SONARR_API_KEY"), auth_token=_env_secret("IMPORTARR_AUTH_TOKEN"), - restart_command=_env_command("IMPORTARR_RESTART_COMMAND", ["systemctl", "restart", "importarr.service"]), + start_command=_env_command("IMPORTARR_START_COMMAND", ["systemctl", "start", "manual-media-import.service"]), + stop_command=_env_command("IMPORTARR_STOP_COMMAND", ["systemctl", "stop", "manual-media-import.service"]), + restart_command=_env_command("IMPORTARR_RESTART_COMMAND", ["systemctl", "restart", "manual-media-import.service"]), update_command=_env_command("IMPORTARR_UPDATE_COMMAND", ["sh", "deploy/repo-upgrade.sh"]), update_release_url=os.getenv("IMPORTARR_UPDATE_RELEASE_URL", "https://gitea.delphas.dk/api/v1/repos/daniels/importarr/releases/latest"), update_check_timeout_seconds=int(os.getenv("IMPORTARR_UPDATE_CHECK_TIMEOUT_SECONDS", "15")), diff --git a/importarr/main.py b/importarr/main.py index 0ed681b..b035dde 100644 --- a/importarr/main.py +++ b/importarr/main.py @@ -258,7 +258,7 @@ def set_queue_control(payload: QueueControlRequest, _: None = Depends(require_wr def start_queue(_: None = Depends(require_write_auth)) -> dict[str, object]: state.set_app_state("queue_mode", "running") state.set_app_state("cancel_requested", "false") - return control_status() + return {"control": control_status(), "command_result": _run_control_command(settings.start_command)} @app.post("/api/control/pause") @@ -270,7 +270,8 @@ def pause_queue(_: None = Depends(require_write_auth)) -> dict[str, object]: @app.post("/api/control/stop") def stop_queue(_: None = Depends(require_write_auth)) -> dict[str, object]: state.set_app_state("queue_mode", "stopped") - return control_status() + state.set_app_state("cancel_requested", "true") + return {"control": control_status(), "command_result": _run_control_command(settings.stop_command)} @app.post("/api/control/cancel-current") diff --git a/tests/test_queue_controls.py b/tests/test_queue_controls.py index e069cd7..6406db9 100644 --- a/tests/test_queue_controls.py +++ b/tests/test_queue_controls.py @@ -42,6 +42,57 @@ def test_start_reenables_manual_queue_sync(tmp_path, monkeypatch): assert len(main.state.list_queue_items()) == 1 +def test_default_control_commands_target_manual_import_service(monkeypatch): + monkeypatch.delenv("IMPORTARR_START_COMMAND", raising=False) + monkeypatch.delenv("IMPORTARR_STOP_COMMAND", raising=False) + monkeypatch.delenv("IMPORTARR_RESTART_COMMAND", raising=False) + + settings = Settings.from_env() + + assert settings.start_command == ["systemctl", "start", "manual-media-import.service"] + assert settings.stop_command == ["systemctl", "stop", "manual-media-import.service"] + assert settings.restart_command == ["systemctl", "restart", "manual-media-import.service"] + + +def test_start_control_starts_manual_import_service(tmp_path, monkeypatch): + main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch) + main.settings.start_command = ["systemctl", "start", "manual-media-import.service"] + + calls = [] + + def fake_run(command, **kwargs): + calls.append(command) + return main.subprocess.CompletedProcess(command, 0, stdout="started", stderr="") + + monkeypatch.setattr(main.subprocess, "run", fake_run) + + result = main.start_queue() + + assert result["control"]["queue_mode"] == "running" + assert result["command_result"]["command"] == ["systemctl", "start", "manual-media-import.service"] + assert calls == [["systemctl", "start", "manual-media-import.service"]] + + +def test_stop_control_stops_manual_import_service(tmp_path, monkeypatch): + main, _download, _movies, _tv = configure_main(tmp_path, monkeypatch) + main.settings.stop_command = ["systemctl", "stop", "manual-media-import.service"] + + calls = [] + + def fake_run(command, **kwargs): + calls.append(command) + return main.subprocess.CompletedProcess(command, 0, stdout="stopped", stderr="") + + monkeypatch.setattr(main.subprocess, "run", fake_run) + + result = main.stop_queue() + + assert result["control"]["queue_mode"] == "stopped" + assert result["control"]["cancel_requested"] is True + assert result["command_result"]["command"] == ["systemctl", "stop", "manual-media-import.service"] + assert calls == [["systemctl", "stop", "manual-media-import.service"]] + + def test_queue_jobs_include_groups_and_manual_context(tmp_path, monkeypatch): main, download, _movies, _tv = configure_main(tmp_path, monkeypatch) batch = download / "Release" / "Season 1"