Mirror download job logs to stdout

This commit is contained in:
Dymas
2026-07-19 15:27:41 +02:00
parent 3f4baf21e8
commit ef2df14aab
8 changed files with 41 additions and 2 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## 0.49.4 - 2026-07-19
- Added optional download job stdout mirroring with `ANI_CLI_WEB_JOB_STDOUT`, enabled by default in Docker and Docker Compose so container log collectors can capture downloader output.
## 0.49.3 - 2026-07-19
- Changed Docker builds to copy the current local `ani-cli-web` checkout into the image instead of cloning the configured remote branch, so rebuilt images include local Docker/runtime fixes immediately.
+1
View File
@@ -14,6 +14,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
ANI_CLI_DOWNLOAD_DIR=/downloads \
ANI_CLI_WEB_HOST=0.0.0.0 \
ANI_CLI_WEB_PORT=8421 \
ANI_CLI_WEB_JOB_STDOUT=true \
UPDATE_ON_START=false \
USER_UID= \
USER_GID=
+3 -1
View File
@@ -2,7 +2,7 @@
Local web UI for a system-wide `ani-cli` install.
Current version: `0.49.3`
Current version: `0.49.4`
## What it does
@@ -49,6 +49,7 @@ Useful flags and env:
- `./ani-cli-web --debug`
- `ANI_CLI_WEB_HOST=0.0.0.0`
- `ANI_CLI_WEB_PORT=8421`
- `ANI_CLI_WEB_JOB_STDOUT=true`
- `ANI_CLI_BIN=/path/to/ani-cli`
- `ANIPY_CLI_BIN=/path/to/anipy-cli`
- `ANIMDL_BIN=/path/to/animdl`
@@ -88,6 +89,7 @@ Docker notes:
- App state is stored under `/app/.ani-cli-web`
- `USER_UID` and `USER_GID` help keep mounted files owned by your host user
- `UPDATE_ON_START=true` runs `ani-cli --update` before startup
- `ANI_CLI_WEB_JOB_STDOUT=true` mirrors download job output to container stdout for log collectors; set it to `false` to keep job output only in the Queue page
- `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 at `/usr/local/bin`, linked from `/usr/bin`, and included in the runtime `PATH`; `animdl` is isolated in a Python 3.11 virtualenv so its pinned dependencies stay executable
+1 -1
View File
@@ -1 +1 @@
0.49.3
0.49.4
+4
View File
@@ -271,6 +271,10 @@ def background_workers_enabled():
return not env_flag("ANI_CLI_WEB_DISABLE_WORKER")
def job_stdout_enabled():
return env_flag("ANI_CLI_WEB_JOB_STDOUT")
def client_address_is_local(host):
text = str(host or "").strip()
if not text:
+1
View File
@@ -20,6 +20,7 @@ services:
ANI_CLI_WEB_ALLOW_REMOTE: ${ANI_CLI_WEB_ALLOW_REMOTE:-false}
ANI_CLI_WEB_AUTH_USERNAME: ${ANI_CLI_WEB_AUTH_USERNAME:-}
ANI_CLI_WEB_AUTH_PASSWORD: ${ANI_CLI_WEB_AUTH_PASSWORD:-}
ANI_CLI_WEB_JOB_STDOUT: ${ANI_CLI_WEB_JOB_STDOUT:-true}
ANI_CLI_DOWNLOAD_DIR: /downloads
volumes:
- ./downloads:/downloads
+12
View File
@@ -29,6 +29,7 @@ from app_support import (
finalize_library_files,
job_output_dir,
job_staging_dir,
job_stdout_enabled,
load_json,
normalize_config,
now_iso,
@@ -1434,6 +1435,7 @@ class DownloadQueue:
text = strip_control(line).strip()
if not text:
return
self._stdout_log(job, text)
with self.lock:
job.setdefault("log", []).append(text)
job["log"] = job["log"][-MAX_LOG_LINES:]
@@ -1447,6 +1449,16 @@ class DownloadQueue:
if should_flush:
self._save_job_locked(job)
def _stdout_log(self, job, line):
if not job_stdout_enabled():
return
text = strip_control(str(line)).strip()
if not text:
return
job_id = str((job or {}).get("id") or "-")
backend = str((job or {}).get("download_backend") or "download")
print(f"[download:{job_id}:{backend}] {text}", flush=True)
def _cleanup_staging_dir(self, job):
staging_dir = job_staging_dir(job)
if not staging_dir.exists():
+15
View File
@@ -381,6 +381,21 @@ class DownloadQueueCancelTests(unittest.TestCase):
self.assertEqual(len(saved), 1)
self.assertEqual(saved[0][-1], "line flush")
def test_append_log_can_mirror_download_output_to_stdout(self):
queue = object.__new__(APP.DownloadQueue)
queue.lock = threading.RLock()
queue._save_job_locked = lambda _job: None
job = {"id": "job-1", "download_backend": "animdl", "log": [], "updated_at": APP.now_iso()}
with mock.patch.dict(os.environ, {"ANI_CLI_WEB_JOB_STDOUT": "true"}, clear=False), mock.patch("builtins.print") as print_mock:
APP.DownloadQueue._append_log(queue, job, "\x1b[32mDownloading episode 1\x1b[0m\n")
print_mock.assert_called_once_with(
"[download:job-1:animdl] Downloading episode 1",
flush=True,
)
self.assertEqual(job["log"], ["Downloading episode 1"])
class QueueApiTests(unittest.TestCase):
def setUp(self):