# ani-cli web Small local web UI for a system-wide `ani-cli` install. Current version: `0.45.3` ## Project Layout ```text ani-cli-web/ app.py Main entrypoint and runtime wiring. app_support.py Shared config, state, and helper utilities. Dockerfile Container image build. docker-compose.yaml Example Compose service. queue_jobs.py Download queue and watchlist refresh workers. watchlist_identity.py Download-to-watchlist identity matching. title_matching.py Shared title normalization and matching. http_handler.py HTTP routing and access control. web_templates.py Shared page aggregation. search_page.py Search page template. queue_page.py Queue page template. config_page.py Config page template. watchlist_page.py Watchlist page template. docker-entrypoint.sh Container startup wrapper. ani-cli-web Launcher script. test_app.py Regression tests. VERSION Project version. CHANGELOG.md Change history. README.md Project documentation. ``` ## Quick Start Run the app: ```sh ./ani-cli-web ``` Run with debug logging: ```sh ./ani-cli-web --debug ``` Open: ```text http://127.0.0.1:8421/ ``` Default bind address is `127.0.0.1:8421`. ## Docker The image installs `ani-cli` system-wide, downloads the latest upstream `yt-dlp` release binary from GitHub, and clones `ani-cli-web` into `/app` during build. Default build args: ```text ANI_CLI=https://github.com/pystardust/ani-cli.git ANI_CLI_BRANCH=master ANI_CLI_WEB=https://gitea.coreplay.eu/Dymas/ani-cli-web.git ``` Build: ```sh docker build -t ani-cli-web . ``` Build with a custom `ani-cli` branch: ```sh docker build \ --build-arg ANI_CLI_BRANCH=my-branch \ --build-arg ANI_CLI=https://github.com/pystardust/ani-cli.git \ --build-arg ANI_CLI_WEB=https://gitea.coreplay.eu/Dymas/ani-cli-web.git \ -t ani-cli-web . ``` Run: ```sh docker run --rm \ -p 8421:8421 \ -e UPDATE_ON_START=true \ -e USER_UID="$(id -u)" \ -e USER_GID="$(id -g)" \ -v "$(pwd)/downloads:/downloads" \ -v "$(pwd)/.ani-cli-web:/app/.ani-cli-web" \ ani-cli-web ``` Compose: ```sh docker compose up --build -d ``` Use host ownership for mounted downloads: ```sh USER_UID="$(id -u)" USER_GID="$(id -g)" docker compose up --build -d ``` Container notes: - `ani-cli` is installed at `/usr/local/bin/ani-cli`. - `yt-dlp` is installed at `/usr/local/bin/yt-dlp` from the latest GitHub release during build. - `ani-cli-web` runs from `/app`. - Downloads go to `/downloads`. - App state lives in `/app/.ani-cli-web`. - The container binds `0.0.0.0:8421`. - `UPDATE_ON_START=true` runs `sudo ani-cli --update` before startup. - `USER_UID` and `USER_GID` keep mounted files from being owned by `root`. ## What It Does - Search AllAnime in `sub` or `dub`. - Queue downloads with editable folder, quality, media type, season, and episode range. - Save defaults on `/config`. - Track shows on `/watchlist`. - Refresh watchlist episode counts in the background. - Schedule periodic watchlist refresh jobs. - Auto-download newly discovered episodes for `Watching` entries after later refreshes. - Send Discord webhook notifications for refresh discoveries and failures. - Send Discord webhook notifications for completed downloads. - Optionally move fully completed watchlist libraries into separate Jellyfin TV and movie folders. - Cache watchlist thumbnails locally. - Sync successful downloads back into the watchlist. - Keep queue and watchlist data in SQLite under `.ani-cli-web/`. - Browse host folders directly from Config page path fields when choosing library locations. ## Pages ### Search Page Path: `/` Use it to: 1. Search for an anime. 2. Load episodes. 3. Adjust folder, quality, media type, season, and episode range. 4. Add the show to the queue or watchlist. Highlights: - Search stays focused on finding shows, loading episodes, and queueing new downloads. - Added-to-queue notices now point to the dedicated Queue page for progress tracking. - Search request guards still avoid stale responses overwriting newer results. - The shared sidebar menu now shows one navigation entry per line for cleaner balance across all pages. ### Queue Page Path: `/queue` Use it to: 1. Monitor active, pending, failed, and finished download jobs. 2. Inspect recent per-job logs with newest lines first. 3. Retry failed jobs. 4. Remove finished jobs. 5. Cancel running or pending jobs. Highlights: - The queue list loads once, then only visible pending or running jobs are refreshed every 2.5 seconds. - Terminal jobs (`done`, `failed`, `canceled`) stop log polling automatically. - Dedicated queue pagination and cleanup controls. - Search and queue workflows are now separated into distinct pages. - Each queue job card can be collapsed, and finished jobs start collapsed by default to keep the page compact. ### Config Page Path: `/config` Use it to set: 1. Default download folder. 2. Default `sub` or `dub` mode. 3. Default quality. 4. Scheduled watchlist refresh enable or disable. 5. Scheduled refresh period in minutes. 6. Delay between shows during `Refresh All`. 7. Global auto-download enable or disable. 8. Default auto-download language and quality. 9. Jellyfin automatic handoff enable or disable. 10. Jellyfin TV and movie library paths plus a manual trigger action. 11. Discord webhook URL. 12. Discord notification events plus a test action. Layout: - `Dependencies` stays at the top as a full-width runtime status card. - The remaining Config sections use two columns on larger screens and collapse to one column at `1080px` width and below. - Runtime info shows both the `ani-cli-web` version and the installed `ani-cli` version. - Path fields include `Browse` buttons that open a host-side folder picker modal in the browser. Saved settings are written to `.ani-cli-web/config.json`. Jellyfin handoff: - Automatic Jellyfin moves run after a watchlist refresh updates an entry and the show now looks complete. - TV entries only move after the anime is marked `Finished` and at least one downloaded language covers the expected episode total. - Movie entries move after they are downloaded and the local movie library folder exists. - The manual `Run now` action on `/config` scans the whole watchlist and moves anything already eligible. Discord notification events: - Download completed. - Jellyfin move succeeded. - Jellyfin move failed. - Refresh finished and found new episodes. - Refresh failed or finished with per-show refresh errors. - Refresh interrupted by restart or shutdown. - Unexpected runtime errors. Webhook details: - Download-completed notifications include the full saved file list, split across extra Discord embeds when needed. - Jellyfin handoff notifications fire when a finished library is moved successfully and also on actionable move failures such as missing targets or move errors. - Watchlist entries only move to `Finished` automatically when the downloaded mode itself is complete, and Jellyfin handoff for TV entries follows that same mode-aware completion rule. - Queue jobs that exit successfully but produce no downloaded files are marked as failed with a clearer explanation instead of looking like a generic rename problem. ### Watchlist Path: `/watchlist` Categories: - `Watching` - `Planned` - `Finished` - `Dropped` Ways to add shows: 1. From the Search page. 2. Manually on `/watchlist`. Main behavior: 1. `Refresh` updates one show. 2. `Refresh All` runs in the background. 3. `Download all` expands an inline picker for `Sub` or `Dub` and then queues every currently available episode directly. 4. Clicking the title opens the source page. 5. `Remove` deletes the entry and cached poster. 6. Each card has inline category and media-type selectors. 7. Summary cards show total tracked shows plus `sub` and `dub` counts. 8. Pagination shows 30 cards per page. 9. `Alternative titles` opens a centered overlay with English alternate titles from AniDB when the watchlist refresh can match the show to an AniDB entry. 10. `Auto-download` opens a centered overlay with per-series settings for language, quality, series number, optional episode offset, and the sanitized library name used for automatically queued episodes. Auto-download layout note: `Name source` and `Name` share a wider half-and-half row inside the modal for easier editing. 11. Adding a show from Search reuses the current `Season` field as the initial auto-download series value for that new watchlist entry. 12. Each watchlist entry can be tagged as `TV` or `Movie`, which changes where finished downloads are stored. Episode offset: - Leave it empty to keep normal episode numbering. - Set it to a starting number such as `13` when a split season continues inside a new folder, so downloaded files are named like `Show Name - S02E13`, `Show Name - S02E14`, and so on. Status behavior: - `Sub` and `Dub` pills turn green when that track looks complete. - Cards with both `sub` and `dub` fully ready get a thin green border. - Finished cards show a small check badge. - Green check means downloaded through the app. - Gray check means finished manually. Download sync: - Existing watchlist entries move to `Finished` when a download succeeds. - Missing entries are added to `Finished`. - If ani-cli resolves a different `show_id`, the app can reconcile a unique title match instead of creating a duplicate finished row. Thumbnail behavior: - Thumbnails are served from the local `/api/watchlist/thumb/...` route. - Cached thumbnails are reused. - Missing covers are warmed in small batches. - Failed lookups back off before retrying. - You can force `Reload` or use `Upload` when no thumbnail exists. Refresh behavior: - Per-show refreshes are queued in the background. - Queued refresh state survives restarts. - Duplicate refresh queueing is avoided. - Auto-download does not trigger when a show is first added to the watchlist. - Auto-download only runs for entries in `Watching` after later manual refreshes or scheduled refreshes. - Auto-download uses stored downloaded-episode history per language, so it can queue any still-missing episodes after a later refresh, including backlog episodes that existed before the show was first tracked. - Scheduled refresh runs log start, stop, success, and failure to stdout. - `Refresh All` works one show at a time with a configurable delay. - When a Discord webhook is configured, new-episode refresh results include one notification card per anime with sub and dub counts and a cached thumbnail attachment when available. ## Access Guard By default the app only serves loopback clients such as `127.0.0.1` and `::1`. To allow LAN or remote access: ```sh ANI_CLI_WEB_ALLOW_REMOTE=1 ANI_CLI_WEB_AUTH_USERNAME=admin ANI_CLI_WEB_AUTH_PASSWORD=choose-a-long-random-password ``` Non-local browser visits are redirected to a sign-in form automatically when no valid session cookie is present. API clients can authenticate with: - `Authorization: Basic ` The browser login stores a persistent HTTP-only session cookie, so you usually only need to sign in once per browser. You can also set the same credentials directly in `.ani-cli-web/config.json`: ```json { "auth_username": "admin", "auth_password": "choose-a-long-random-password" } ``` Environment variables take precedence over values saved in `config.json`. ## Download Queue Path: `/queue` The queue is stored in SQLite, shown 10 jobs at a time, and managed from its dedicated page. Available actions: - `Retry` - `Cancel` - `Remove` - `Retry failed` - `Remove finished` Downloads are staged first, then moved into a media-friendly layout: ```text ANI_CLI_DOWNLOAD_DIR/tv/anime_name/Season 01/anime_name - S01E01.mp4 ANI_CLI_DOWNLOAD_DIR/movie/anime_name/anime_name.mp4 ``` ## Runtime State All local state is stored under: ```text ani-cli-web/.ani-cli-web/ ``` Main contents: - `config.json` - `state.sqlite3` - `thumbnails/` - AniDB title cache data - staging files for active downloads Legacy state from older locations is migrated on startup. ## Configuration Environment variables: - `ANI_CLI_BIN`: path or command name for `ani-cli` - `ANI_CLI_DOWNLOAD_DIR`: initial default download folder - `ANI_CLI_MODE`: initial mode, `sub` or `dub` - `ANI_CLI_QUALITY`: initial quality such as `best`, `1080`, or `720` - `ANI_CLI_WEB_HOST`: server host, default `127.0.0.1` - `ANI_CLI_WEB_PORT`: server port, default `8421` - `ANI_CLI_WEB_ALLOW_REMOTE`: allow non-loopback clients - `ANI_CLI_WEB_AUTH_USERNAME`: single remote-login username - `ANI_CLI_WEB_AUTH_PASSWORD`: single remote-login password - `ANI_CLI_WEB_DEBUG`: enable debug logging - `ANI_CLI_WEB_STATE_ROOT`: override the project-local state directory - `UPDATE_ON_START`: run `sudo ani-cli --update` in the container before startup - `USER_UID` and `USER_GID`: run the container as a specific host user/group Saved config fields include: - `auth_username` - `auth_password` - `jellyfin_sync_enabled` - `jellyfin_tv_dir` - `jellyfin_movie_dir` - `discord_webhook_url` - `discord_webhook_events` - `watchlist_auto_refresh_enabled` - `watchlist_auto_refresh_minutes` - `watchlist_refresh_delay_seconds` ## Testing Run the regression suite with: ```sh python3 -m unittest test_app.py ``` ## Notes - Runtime boot is lazy. Importing `app.py` does not immediately start worker threads. - Queue and watchlist data share the same SQLite database: `.ani-cli-web/state.sqlite3`. - Runtime folders such as `.ani-cli-web/` and `downloads/` are ignored by git. - The Docker image clones both upstream repositories during build.