Fix animdl discovery in Docker runtime

This commit is contained in:
Dymas
2026-07-19 14:31:16 +02:00
parent dbc1dc8e3a
commit 02226ad5c9
7 changed files with 82 additions and 8 deletions
+5
View File
@@ -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.
+2
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
0.49.0
0.49.1
+18 -3
View File
@@ -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"
+29 -2
View File
@@ -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:
+25
View File
@@ -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: {