diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aa26bb..cd4a217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.49.0 - 2026-07-19 + +- Added `animdl` as a third queue download backend and Docker-installed fallback tool. +- Replaced the single `anipy-cli` fallback toggle with selectable download methods for `ani-cli`, `anipy-cli`, and `animdl`; queue jobs try the checked methods in that order. +- Exposed `animdl` availability and version in the Config page runtime checks. + ## 0.48.3 - 2026-07-17 - Added Jellyfin handoff jobs to the Queue page so manual copy jobs appear alongside downloads with live progress, moved-file counts, and recent handoff activity. diff --git a/Dockerfile b/Dockerfile index 5dd0377..0ca028d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,8 +49,9 @@ RUN curl -fL "${YT_DLP_RELEASE_URL}" -o /usr/local/bin/yt-dlp \ && chmod 0755 /usr/local/bin/yt-dlp \ && yt-dlp --version -RUN python3 -m pip install --no-cache-dir anipy-cli \ - && anipy-cli --version +RUN python3 -m pip install --no-cache-dir anipy-cli animdl \ + && anipy-cli --version \ + && animdl --version RUN git clone --depth 1 --branch "${ANI_CLI_WEB_BRANCH}" "${ANI_CLI_WEB}" /app \ && rm -rf /app/.git \ diff --git a/README.md b/README.md index 212bafb..c53d8fc 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ Local web UI for a system-wide `ani-cli` install. -Current version: `0.48.3` +Current version: `0.49.0` ## What it does - Search anime in `sub` or `dub`, with poster-style results shown below the selection panel and the first English alternate title when available - Queue downloads with folder, quality, media type, season, and episode controls -- Optionally retry failed `ani-cli` downloads with `anipy-cli` +- Choose which download backends queue jobs may use: `ani-cli`, `anipy-cli`, and `animdl` - Track shows in a watchlist with `Watching`, `Planned`, `Finished`, and `Dropped` categories - Expose a small JSON summary endpoint for Homepage dashboard widgets - Refresh watchlist episode counts manually or on a schedule @@ -22,14 +22,14 @@ Current version: `0.48.3` - `/`: Search and queue downloads, with poster-card search results below the selection box and alternate English titles under the main name when available - `/queue`: Monitor downloads and Jellyfin handoff jobs, inspect logs, retry failed downloads, avoid duplicate re-queues for the same failed download, clear failed jobs, and remove finished jobs - `/watchlist`: Track shows, refresh them, download all available episodes, and manage auto-download settings such as `Source name`, library `Name`, season, and episode offset -- `/config`: Save defaults, toggle `anipy-cli` fallback retries, schedule refreshes, configure Jellyfin and Discord, monitor Jellyfin handoff progress, and view runtime info and the changelog +- `/config`: Save defaults, choose queue download methods and fallback retries, schedule refreshes, configure Jellyfin and Discord, monitor Jellyfin handoff progress, and view runtime info and the changelog ## Quick start Requirements: - `ani-cli` installed and available in `PATH` -- Optional: `anipy-cli` installed and available in `PATH` if you want automatic fallback retries outside Docker +- Optional: `anipy-cli` and/or `animdl` installed and available in `PATH` if you want automatic fallback retries outside Docker - Common runtime tools used by `ani-cli`, especially `curl`, `ffmpeg`, `fzf`, `grep`, `openssl`, `sed`, `aria2c`, `botan`, and `yt-dlp` Run locally: @@ -51,6 +51,7 @@ Useful flags and env: - `ANI_CLI_WEB_PORT=8421` - `ANI_CLI_BIN=/path/to/ani-cli` - `ANIPY_CLI_BIN=/path/to/anipy-cli` +- `ANIMDL_BIN=/path/to/animdl` ## Docker @@ -95,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` is installed systemwide inside the container, so fallback retries are available without extra container setup +- `anipy-cli` and `animdl` are installed systemwide inside the container, so fallback retries are available without extra container setup ## Key behavior @@ -117,9 +118,11 @@ Docker notes: ### Download fallback - Optional and configured from `/config` -- When enabled, a failed `ani-cli` queue job retries once with `anipy-cli` -- Fallback retries keep the queued episode numbers when the downloaded `anipy-cli` filenames are renumbered from `1` -- Docker images install `anipy-cli` automatically; non-Docker installs should provide it in `PATH` themselves +- Queue jobs can use any checked combination of `ani-cli`, `anipy-cli`, and `animdl` +- Checked methods are tried in this order: `ani-cli`, `anipy-cli`, then `animdl` +- Selecting one method disables fallback retry; selecting multiple methods makes later methods automatic fallbacks +- Fallback retries keep the queued episode numbers when fallback downloader filenames are renumbered from `1` +- Docker images install `anipy-cli` and `animdl` automatically; non-Docker installs should provide the selected tools in `PATH` themselves ### Queue retries @@ -162,7 +165,7 @@ The Config page runtime panel shows: - `ani-cli-web` version loaded from `VERSION` - Installed `ani-cli` version -- Installed `anipy-cli` version and dependency status +- Installed `anipy-cli` and `animdl` versions and dependency status - A `Changelog` button that opens a scrollable viewer backed by `CHANGELOG.md` ## Remote access @@ -211,6 +214,7 @@ Most users only need these environment variables: - `ANI_CLI_BIN` - `ANIPY_CLI_BIN` +- `ANIMDL_BIN` - `ANI_CLI_WEB_HOST` - `ANI_CLI_WEB_PORT` - `ANI_CLI_WEB_ALLOW_REMOTE` diff --git a/VERSION b/VERSION index c373fc9..5c4503b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.48.3 +0.49.0 diff --git a/app.py b/app.py index 5f0d4f1..1bf807a 100644 --- a/app.py +++ b/app.py @@ -2766,6 +2766,8 @@ def save_runtime_config(config): current = get_config_snapshot(fallback=DEFAULT_CONFIG) merged = dict(current) if isinstance(config, dict): + if "anipy_cli_fallback_enabled" in config and "download_methods" not in config: + merged.pop("download_methods", None) merged.update(config) normalized = normalize_config(merged) previous_fingerprint = remote_access_session_fingerprint(current) diff --git a/app_support.py b/app_support.py index 93174c8..c1f0cc0 100644 --- a/app_support.py +++ b/app_support.py @@ -24,6 +24,7 @@ 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" APP_NAME = "ani-cli-web" VERSION_FILE = PROJECT_ROOT / "VERSION" CHANGELOG_FILE = PROJECT_ROOT / "CHANGELOG.md" @@ -41,6 +42,7 @@ DEBUG_MODE = False QUALITY_CHOICES = {"best", "1080", "1080p", "720", "720p", "480", "480p", "360", "360p", "worst"} MODE_CHOICES = {"sub", "dub"} +DOWNLOAD_METHOD_CHOICES = ("ani-cli", "anipy-cli", "animdl") MEDIA_TYPE_CHOICES = {"tv", "movie"} WATCHLIST_CATEGORY_LABELS = { "watching": "Watching", @@ -292,6 +294,7 @@ DEFAULT_CONFIG = { "mode": os.environ.get("ANI_CLI_MODE", "sub"), "quality": os.environ.get("ANI_CLI_QUALITY", "best"), "anipy_cli_fallback_enabled": False, + "download_methods": ["ani-cli"], "watchlist_auto_refresh_enabled": False, "watchlist_auto_refresh_minutes": WATCHLIST_AUTO_REFRESH_DEFAULT_MINUTES, "watchlist_refresh_delay_seconds": WATCHLIST_REFRESH_DELAY_DEFAULT_SECONDS, @@ -438,6 +441,7 @@ def write_json(path, data): def normalize_config(data): + source_has_download_methods = isinstance(data, dict) and "download_methods" in data config = dict(DEFAULT_CONFIG) if isinstance(data, dict): config.update({key: data[key] for key in DEFAULT_CONFIG if key in data}) @@ -448,6 +452,7 @@ def normalize_config(data): quality = quality[:-1] download_dir = str(config.get("download_dir") or DEFAULT_CONFIG["download_dir"]) anipy_cli_fallback_enabled = config.get("anipy_cli_fallback_enabled") + download_methods = config.get("download_methods") auto_refresh_enabled = config.get("watchlist_auto_refresh_enabled") auto_refresh_minutes = config.get("watchlist_auto_refresh_minutes") refresh_delay_seconds = config.get("watchlist_refresh_delay_seconds") @@ -468,6 +473,18 @@ def normalize_config(data): anipy_cli_fallback_enabled = anipy_cli_fallback_enabled.strip().lower() in {"1", "true", "yes", "on"} else: anipy_cli_fallback_enabled = bool(anipy_cli_fallback_enabled) + if isinstance(download_methods, str): + method_values = [part.strip().lower() for part in re.split(r"[\s,]+", download_methods) if part.strip()] + elif isinstance(download_methods, list): + method_values = [str(part or "").strip().lower() for part in download_methods] + else: + method_values = [] + normalized_methods = [] + for method in method_values: + if method in DOWNLOAD_METHOD_CHOICES and method not in normalized_methods: + normalized_methods.append(method) + if not normalized_methods or not source_has_download_methods: + normalized_methods = ["ani-cli", "anipy-cli"] if anipy_cli_fallback_enabled else ["ani-cli"] if isinstance(auto_refresh_enabled, str): auto_refresh_enabled = auto_refresh_enabled.strip().lower() in {"1", "true", "yes", "on"} else: @@ -501,7 +518,8 @@ def normalize_config(data): config["mode"] = mode if mode in MODE_CHOICES else "sub" config["quality"] = quality if quality in QUALITY_CHOICES else "best" config["download_dir"] = str(Path(download_dir).expanduser()) - config["anipy_cli_fallback_enabled"] = anipy_cli_fallback_enabled + config["download_methods"] = normalized_methods + config["anipy_cli_fallback_enabled"] = len(normalized_methods) > 1 config["watchlist_auto_refresh_enabled"] = auto_refresh_enabled config["watchlist_auto_refresh_minutes"] = auto_refresh_minutes config["watchlist_refresh_delay_seconds"] = refresh_delay_seconds @@ -1131,7 +1149,7 @@ def finalize_library_files(job): final_name = f"{library_name}{source.suffix}" else: ep = None - if backend == "anipy-cli" and requested_episode_values: + if backend in {"anipy-cli", "animdl"} and requested_episode_values: ep = requested_episode_values.pop(0) if ep is None: ep = extract_episode_from_filename(source) @@ -1211,6 +1229,22 @@ def command_for_job(job, backend="ani-cli", download_path=None): if download_path: command.extend(["-l", str(download_path)]) return command + if normalized_backend == "animdl": + query = str(job.get("query") or "").strip() + command = [ + ANIMDL, + "download", + query, + "-r", + job["episodes"], + "-q", + job["quality"], + "--index", + "1", + ] + if download_path: + command.extend(["-d", str(download_path)]) + return command command = [ ANI_CLI, "-d", diff --git a/config_page.py b/config_page.py index d7f95eb..6620e33 100644 --- a/config_page.py +++ b/config_page.py @@ -463,6 +463,7 @@ CONFIG_HTML = r"""
Loading version...
Detecting ani-cli version...
Detecting anipy-cli fallback...
+Detecting animdl fallback...
Saved settings live in `.ani-cli-web/config.json` inside this project.
@@ -512,18 +513,15 @@ CONFIG_HTML = r""" -