diff --git a/CHANGELOG.md b/CHANGELOG.md index cd4a217..44ec96d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.49.1 - 2026-07-19 + +- Fixed Docker fallback tool discovery by exposing `anipy-cli` and `animdl` from fixed system paths and checking common system install locations at runtime. +- Changed queue jobs to fail immediately with a clear message when every selected download method is unavailable, instead of trying to launch an unavailable command. + ## 0.49.0 - 2026-07-19 - Added `animdl` as a third queue download backend and Docker-installed fallback tool. diff --git a/Dockerfile b/Dockerfile index 8c198a8..1ff213d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,6 +60,8 @@ RUN python3 -m pip install \ animdl \ && test -x /usr/local/bin/anipy-cli \ && test -x /usr/local/bin/animdl \ + && ln -sf /usr/local/bin/anipy-cli /usr/bin/anipy-cli \ + && ln -sf /usr/local/bin/animdl /usr/bin/animdl \ && /usr/local/bin/anipy-cli --version \ && /usr/local/bin/animdl --version diff --git a/README.md b/README.md index c53d8fc..3b57ecf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Local web UI for a system-wide `ani-cli` install. -Current version: `0.49.0` +Current version: `0.49.1` ## What it does @@ -96,7 +96,7 @@ Docker notes: - `UPDATE_ON_START=true` runs `ani-cli --update` before startup - `ANI_CLI_WEB_BRANCH` lets the image clone a specific `ani-cli-web` branch at build time - `botan` is installed inside the container for providers or scripts that need Botan cryptography tooling -- `anipy-cli` and `animdl` are installed systemwide inside the container, so fallback retries are available without extra container setup +- `anipy-cli` and `animdl` are installed systemwide inside the container at `/usr/local/bin` and linked from `/usr/bin`, so fallback retries are available without extra container setup ## Key behavior diff --git a/VERSION b/VERSION index 5c4503b..36328c4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.49.0 +0.49.1 diff --git a/app_support.py b/app_support.py index c1f0cc0..9d7ce5b 100644 --- a/app_support.py +++ b/app_support.py @@ -22,9 +22,24 @@ from uuid import uuid4 PROJECT_ROOT = Path(__file__).resolve().parent -ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli" -ANIPY_CLI = os.environ.get("ANIPY_CLI_BIN") or shutil.which("anipy-cli") or "anipy-cli" -ANIMDL = os.environ.get("ANIMDL_BIN") or shutil.which("animdl") or "animdl" + + +def configured_cli_command(env_name, executable_name): + configured = str(os.environ.get(env_name) or "").strip() + if configured: + return configured + resolved = shutil.which(executable_name) + if resolved: + return resolved + for candidate in (Path("/usr/local/bin") / executable_name, Path("/usr/bin") / executable_name): + if candidate.exists() and os.access(candidate, os.X_OK): + return str(candidate) + return executable_name + + +ANI_CLI = configured_cli_command("ANI_CLI_BIN", "ani-cli") +ANIPY_CLI = configured_cli_command("ANIPY_CLI_BIN", "anipy-cli") +ANIMDL = configured_cli_command("ANIMDL_BIN", "animdl") APP_NAME = "ani-cli-web" VERSION_FILE = PROJECT_ROOT / "VERSION" CHANGELOG_FILE = PROJECT_ROOT / "CHANGELOG.md" diff --git a/queue_jobs.py b/queue_jobs.py index f9330e3..5aff0de 100644 --- a/queue_jobs.py +++ b/queue_jobs.py @@ -1535,8 +1535,8 @@ class DownloadQueue: unavailable_methods.append(method) continue attempts.append(attempt_for(method)) - if not attempts: - attempts.append(attempt_for(selected_methods[0])) + if not attempts and selected_methods == ["ani-cli"]: + attempts.append(attempt_for("ani-cli")) return attempts, selected_methods, unavailable_methods def _run(self): @@ -1554,6 +1554,33 @@ class DownloadQueue: staging_dir.mkdir(parents=True, exist_ok=True) target_dir.mkdir(parents=True, exist_ok=True) attempts, selected_methods, unavailable_methods = self._command_attempts(job, staging_dir) + if not attempts: + with self.lock: + job["status"] = "failed" + job["started_at"] = now_iso() + job["finished_at"] = job["started_at"] + job["updated_at"] = job["finished_at"] + job["exit_code"] = 1 + job["pid"] = None + job["staging_dir"] = str(staging_dir) + job["target_dir"] = str(target_dir) + job["download_backend"] = selected_methods[0] if selected_methods else None + job["fallback_used"] = False + job["log"] = [ + f"Staging in: {job['staging_dir']}", + f"Final folder: {job['target_dir']}", + f"Download methods: {', '.join(selected_methods)}", + ( + "No selected download methods are installed or executable: " + f"{', '.join(unavailable_methods or selected_methods)}." + ), + ] + self.current_process = None + self.current_job_id = None + self.current_job = None + self._save_job_locked(job) + self._cleanup_staging_dir(job) + return primary_command = attempts[0]["command"] with self.lock: diff --git a/test_app.py b/test_app.py index c1dad45..64be319 100644 --- a/test_app.py +++ b/test_app.py @@ -1036,6 +1036,31 @@ class DownloadQueueWorkerFailureTests(unittest.TestCase): self.assertEqual(commands[1][0], APP.app_support.ANIPY_CLI) self.assertIn("Fallback download completed with anipy-cli.", stored["log"]) + def test_selected_unavailable_animdl_fails_without_start_attempt(self): + queue = APP.DownloadQueue( + lambda: { + "mode": "sub", + "quality": "best", + "download_dir": "/tmp/example", + "download_methods": ["animdl"], + }, + start_worker=False, + ) + job = queue.add({"query": "Queue Show", "title": "Queue Show", "season": "1", "episodes": "1"}) + staging_dir = queue_jobs.job_staging_dir(job) + + with mock.patch.object(queue_jobs, "cli_executable_exists", return_value=False), mock.patch.object( + queue_jobs.subprocess, "Popen" + ) as popen: + queue._run_job(job) + + stored = queue._find(job["id"]) + self.assertEqual(stored["status"], "failed") + self.assertEqual(stored["download_backend"], "animdl") + self.assertIn("No selected download methods are installed or executable: animdl.", stored["log"]) + self.assertFalse(staging_dir.exists()) + popen.assert_not_called() + def test_selected_download_methods_retry_through_animdl(self): queue = APP.DownloadQueue( lambda: {