diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dddd40..28455fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Dockerfile b/Dockerfile index 07b36e0..a5fb137 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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= diff --git a/README.md b/README.md index c9c0bf0..8db24b5 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.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 diff --git a/VERSION b/VERSION index 72c9da1..947e8cd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.49.3 +0.49.4 diff --git a/app_support.py b/app_support.py index 4692664..3107fda 100644 --- a/app_support.py +++ b/app_support.py @@ -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: diff --git a/docker-compose.yaml b/docker-compose.yaml index 6d9f8fe..bd37fbc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 diff --git a/queue_jobs.py b/queue_jobs.py index 5aff0de..7fc3da7 100644 --- a/queue_jobs.py +++ b/queue_jobs.py @@ -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(): diff --git a/test_app.py b/test_app.py index a9a2deb..3cbf870 100644 --- a/test_app.py +++ b/test_app.py @@ -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):