Fix non-interactive anikoto backup downloads
- force anikoto-cli backup runs to auto-select the queued anime title through fzf filter options - add a regression test for the backup retry environment - bump the project version to 0.46.3 and document the fix in the changelog and README
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 0.46.3 - 2026-07-09
|
||||
|
||||
- Fixed backup `anikoto-cli` queue runs to force `fzf` into exact-match auto-select mode, which keeps the fallback downloader non-interactive inside the background worker instead of failing with `nothing selected` and `inappropriate ioctl for device`.
|
||||
|
||||
## 0.46.2 - 2026-07-09
|
||||
|
||||
- Changed the backup downloader setting on the Config page from a hard-to-spot select field into a dedicated checkbox toggle so the enable/disable control is clearly visible.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Local web UI for a system-wide `ani-cli` install.
|
||||
|
||||
Current version: `0.46.2`
|
||||
Current version: `0.46.3`
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -125,6 +125,7 @@ Docker notes:
|
||||
- Configure the global backup toggle from `/config`
|
||||
- When enabled, any failed `ani-cli` queue job automatically retries with `anikoto-cli`
|
||||
- `anikoto-cli` ignores the saved quality setting, so backup attempts use the source's default stream quality
|
||||
- Backup retries now auto-select the queued anime title non-interactively so the fallback works inside the background download worker
|
||||
|
||||
### Jellyfin handoff
|
||||
|
||||
|
||||
+15
-1
@@ -30,6 +30,7 @@ from app_support import (
|
||||
now_iso,
|
||||
printable_command,
|
||||
send_discord_webhook_event,
|
||||
sh_quote,
|
||||
strip_control,
|
||||
)
|
||||
|
||||
@@ -1346,6 +1347,18 @@ class DownloadQueue:
|
||||
attempts.append(("anikoto-cli", lambda job: command_for_job(job, downloader="anikoto-cli")))
|
||||
return attempts
|
||||
|
||||
def _attempt_env(self, base_env, job, downloader_name):
|
||||
attempt_env = dict(base_env)
|
||||
if downloader_name == "anikoto-cli":
|
||||
query = str(job.get("query") or job.get("title") or "").strip()
|
||||
if query:
|
||||
# Force fzf into non-interactive exact-match mode so backup downloads
|
||||
# can auto-select the intended anime inside the queue worker.
|
||||
attempt_env["FZF_DEFAULT_OPTS"] = (
|
||||
f"--exact --filter={sh_quote(query)} --select-1 --exit-0"
|
||||
)
|
||||
return attempt_env
|
||||
|
||||
def _fail_job_startup(self, job, exc):
|
||||
message = f"Could not start download: {exc}"
|
||||
with self.lock:
|
||||
@@ -1455,10 +1468,11 @@ class DownloadQueue:
|
||||
self._append_log(job, f"Launching {downloader_name}: {job['command']}")
|
||||
|
||||
try:
|
||||
attempt_env = self._attempt_env(env, job, downloader_name)
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cwd=str(PROJECT_ROOT),
|
||||
env=env,
|
||||
env=attempt_env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
|
||||
+14
-1
@@ -695,7 +695,15 @@ class DownloadQueueWorkerFailureTests(unittest.TestCase):
|
||||
def wait(self):
|
||||
return 0
|
||||
|
||||
with mock.patch.object(queue_jobs.subprocess, "Popen", side_effect=[FailedProc(), SuccessProc()]), mock.patch.object(
|
||||
popen_calls = []
|
||||
|
||||
def fake_popen(command, **kwargs):
|
||||
popen_calls.append({"command": command, "env": dict(kwargs.get("env") or {})})
|
||||
if len(popen_calls) == 1:
|
||||
return FailedProc()
|
||||
return SuccessProc()
|
||||
|
||||
with mock.patch.object(queue_jobs.subprocess, "Popen", side_effect=fake_popen), mock.patch.object(
|
||||
queue, "_command_available", return_value=True
|
||||
), mock.patch.object(
|
||||
queue_jobs, "finalize_library_files", return_value=["/tmp/example/tv/Queue Show/Season 01/Queue Show - S01E01.mp4"]
|
||||
@@ -708,6 +716,11 @@ class DownloadQueueWorkerFailureTests(unittest.TestCase):
|
||||
self.assertIn("Retrying with backup downloader: anikoto-cli.", log_text)
|
||||
self.assertIn("anikoto-cli does not support ani-cli quality flags", log_text)
|
||||
self.assertIn("Download completed.", log_text)
|
||||
self.assertEqual(len(popen_calls), 2)
|
||||
self.assertNotIn("FZF_DEFAULT_OPTS", popen_calls[0]["env"])
|
||||
self.assertIn("--exact", popen_calls[1]["env"]["FZF_DEFAULT_OPTS"])
|
||||
self.assertIn("--select-1", popen_calls[1]["env"]["FZF_DEFAULT_OPTS"])
|
||||
self.assertIn("Queue Show", popen_calls[1]["env"]["FZF_DEFAULT_OPTS"])
|
||||
|
||||
def test_run_job_marks_post_start_exception_failed_without_killing_worker_state(self):
|
||||
queue = APP.DownloadQueue(lambda: {"mode": "sub", "quality": "best", "download_dir": "/tmp/example"}, start_worker=False)
|
||||
|
||||
Reference in New Issue
Block a user