407 lines
21 KiB
Markdown
407 lines
21 KiB
Markdown
# ani-cli web
|
|
|
|
A local web UI for a system-wide `ani-cli` install.
|
|
|
|
Current version: `0.31.1`
|
|
|
|
## Project Layout
|
|
|
|
```text
|
|
ani-cli-web/
|
|
app.py Public entrypoint, runtime wiring, watchlist domain logic, and CLI startup.
|
|
app_support.py Shared runtime/state/config helpers plus queue and library file utilities.
|
|
Dockerfile Container image build for system-wide ani-cli plus /app ani-cli-web.
|
|
docker-compose.yaml Compose service for building and running the container with persistent mounts.
|
|
queue_jobs.py Download queue and background watchlist-refresh workers.
|
|
watchlist_identity.py Safer watchlist identity resolution for queued download jobs.
|
|
title_matching.py Shared anime-title normalization and matching helpers.
|
|
http_handler.py HTTP route handling, request parsing, and remote-access guard.
|
|
web_templates.py Shared Search, Config, and Watchlist page markup aggregation.
|
|
docker-entrypoint.sh Container startup wrapper with optional ani-cli self-update.
|
|
search_page.py Search page template.
|
|
config_page.py Config page template.
|
|
watchlist_page.py Watchlist page template.
|
|
template_helpers.py Shared page-shell navigation and sidebar brand helpers.
|
|
ani-cli-web Launcher script.
|
|
test_app.py Regression tests with isolated temp-state setup.
|
|
VERSION Project version.
|
|
CHANGELOG.md Change history.
|
|
README.md Project documentation.
|
|
```
|
|
|
|
## Run
|
|
|
|
Start the web app with:
|
|
|
|
```sh
|
|
./ani-cli-web
|
|
```
|
|
|
|
For thumbnail debugging, run:
|
|
|
|
```sh
|
|
./ani-cli-web --debug
|
|
```
|
|
|
|
The launcher also accepts `---debug`. You can also enable debug logging with `ANI_CLI_WEB_DEBUG=1`.
|
|
|
|
Then open:
|
|
|
|
```text
|
|
http://127.0.0.1:8421/
|
|
```
|
|
|
|
By default the server binds to `127.0.0.1:8421`.
|
|
|
|
## Docker
|
|
|
|
The container image installs `ani-cli` system-wide and clones `ani-cli-web` into `/app` during `docker build`.
|
|
|
|
Default build arguments:
|
|
|
|
```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 the image:
|
|
|
|
```sh
|
|
docker build -t ani-cli-web .
|
|
```
|
|
|
|
Override the `ani-cli` branch when needed:
|
|
|
|
```sh
|
|
docker build \
|
|
--build-arg ANI_CLI_BRANCH=my-feature-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 the container with the requested persistent mounts:
|
|
|
|
```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
|
|
```
|
|
|
|
Or start the same setup with Compose:
|
|
|
|
```sh
|
|
docker compose up --build -d
|
|
```
|
|
|
|
To keep mounted downloads owned by your host user, start Compose with:
|
|
|
|
```sh
|
|
USER_UID="$(id -u)" USER_GID="$(id -g)" docker compose up --build -d
|
|
```
|
|
|
|
The included `docker-compose.yaml` uses the same defaults as the `Dockerfile`, mounts `./downloads` to `/downloads`, mounts `./.ani-cli-web` to `/app/.ani-cli-web`, and lets you override build/runtime settings through environment variables such as:
|
|
|
|
```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
|
|
ANI_CLI_WEB_PORT=8421
|
|
UPDATE_ON_START=false
|
|
USER_UID=
|
|
USER_GID=
|
|
ANI_CLI_WEB_ALLOW_REMOTE=false
|
|
ANI_CLI_WEB_REMOTE_TOKEN=
|
|
```
|
|
|
|
Container defaults:
|
|
|
|
- `ani-cli` is installed at `/usr/local/bin/ani-cli`.
|
|
- `ani-cli-web` is cloned into `/app` and started from there.
|
|
- Container startup now runs `python3 /app/app.py` directly, so it does not depend on the `ani-cli-web` launcher file having executable permissions inside the image.
|
|
- Downloads are written to `/downloads`, so bind-mount `./downloads:/downloads`.
|
|
- App state stays under `/app/.ani-cli-web`, so bind-mount `./.ani-cli-web:/app/.ani-cli-web`.
|
|
- The image sets `ANI_CLI_WEB_HOST=0.0.0.0` and exposes port `8421`.
|
|
- `UPDATE_ON_START=true` runs `sudo ani-cli --update` before the web app starts.
|
|
- Set `USER_UID` and `USER_GID` together to run the web app as a specific numeric host user/group, which keeps new files on mounted folders such as `./downloads` from being owned by `root`.
|
|
|
|
The installed `ani-cli` update URL is rewritten during the image build so `ani-cli --update` follows the same `ANI_CLI_BRANCH` you built into the image when the `ANI_CLI` remote is a GitHub repository.
|
|
|
|
## Overview
|
|
|
|
`ani-cli-web` keeps search and download management on `/`, saved defaults on `/config`, and the watchlist on `/watchlist`, all with the same sidebar navigation layout and a Tsuki-inspired visual style.
|
|
|
|
The app currently includes:
|
|
|
|
- Search against AllAnime with `sub` or `dub` mode selection.
|
|
- Episode loading for the selected result.
|
|
- Queueing downloads with editable library name, season, episode range, quality, and target folder.
|
|
- A dedicated Config page for saved default folder, mode, and quality settings.
|
|
- Malformed config save requests now return `400 Bad Request` when the JSON body is not an object, instead of resetting values back to defaults.
|
|
- A paged SQLite-backed download queue with retry, cancel, remove, retry-failed, and clear-finished actions.
|
|
- A paged SQLite-backed watchlist with summary cards, per-show refresh, bulk refresh, source links, and thumbnail tools.
|
|
- Duplicate watchlist adds now return `200 OK` with `created: false`, keeping the API status aligned with the no-op result.
|
|
- Malformed watchlist add requests now also return `400 Bad Request` when the JSON body is not an object.
|
|
- Watchlist category tabs for `Watching`, `Planned`, `Finished`, and `Dropped`, with per-anime category editing.
|
|
- AnimeSchedule-backed watchlist completion tracking that compares live sub/dub availability with the expected total episode count when known.
|
|
- Automatic watchlist sync after successful downloads, including downloaded-vs-manual finished markers.
|
|
- Per-show background watchlist refresh queueing so adding a show or syncing a finished download does not block the request on upstream episode, schedule, and thumbnail lookups.
|
|
- Queued per-show watchlist refresh work now survives app restarts and resumes from rows still marked `queued`.
|
|
- Per-show watchlist refresh queueing now also deduplicates in-flight refreshes, so the same show is not fetched twice back-to-back while one refresh is already running.
|
|
- The watchlist page now silently refreshes itself while queued per-show refresh work is still being processed, so queued entries settle into their updated state without a manual reload.
|
|
- The watchlist page now also avoids re-triggering the same unresolved thumbnail warm-up request on every silent poll cycle while a show's thumbnail state is unchanged.
|
|
- Background `Refresh All` execution with status polling, parallel per-show refresh work, and no inline thumbnail downloads during the bulk job.
|
|
- Optional scheduled background watchlist refreshes with Config page controls for enable/disable and refresh period.
|
|
- Scheduled background refresh runs now log queue, start, and completion status to stdout so container logs show whether they finished successfully, with errors, or failed.
|
|
- Search and Watchlist polling now use a shared serial browser polling helper so slow responses do not cause overlapping poll requests to pile up in the background.
|
|
- That shared browser polling helper now also tears itself down on page unload and uses chained `setTimeout` scheduling instead of a bare repeating interval.
|
|
- Controlled shutdown and runtime reset now cancel active download workers when waiting for teardown, reducing the chance of orphaned background downloads outliving the web process.
|
|
- Runtime reset now also refuses to clear the active global runtime when background workers fail to stop cleanly, preventing stale workers from outliving the runtime object that created them.
|
|
- Watchlist refresh networking now uses a shorter dedicated timeout, which helps per-show and bulk watchlist shutdown paths settle more predictably when upstream metadata services are slow.
|
|
- Persistent watchlist refresh history that survives restarts and marks interrupted bulk refresh jobs clearly.
|
|
- Local thumbnail caching plus manual thumbnail upload when automatic cover lookup fails.
|
|
- A refreshed glassy dark UI inspired by the Tsuki frontend design language.
|
|
- Shared page templates and backend services split into dedicated modules while keeping `app.py` and `web_templates.py` as stable public aggregation points.
|
|
- Broader regression coverage for higher-risk queue, watchlist, remote-access, and handler flows.
|
|
- Search and watchlist requests guard against stale out-of-order async responses during rapid repeated actions.
|
|
- Search-page queue polling also guards against stale out-of-order refresh responses now.
|
|
- Queue log writes are batched during active downloads to reduce SQLite churn on noisy jobs.
|
|
- Queue persistence now stores core job metadata and logs in structured SQLite columns instead of treating the whole job as one large JSON blob.
|
|
- Queue worker startup failures now degrade into a normal failed job state instead of crashing the background queue thread.
|
|
- Unexpected post-launch queue worker failures now also fail the active job cleanly instead of leaving it stuck in `running`.
|
|
- Failed and canceled downloads now clean their project-local staging directories automatically so partial files do not accumulate under `.ani-cli-web/staging/`.
|
|
- Failed thumbnail refresh attempts now record a retry timestamp, so unresolved covers back off instead of immediately requerying upstream sources on every later warm-up.
|
|
- Download-to-watchlist fallback matching now keeps season and part identity intact while still tolerating equivalent season notation such as `2nd Season` vs `Season 2`.
|
|
- Project-local runtime state under `.ani-cli-web/`.
|
|
|
|
## Search Page
|
|
|
|
The main page at `/` is where you search and queue downloads.
|
|
|
|
Workflow:
|
|
|
|
1. Search by anime title or keyword.
|
|
2. Pick a result to load available episodes.
|
|
3. Adjust library name, season, episode range, quality, and download folder.
|
|
4. Add the selected show either to the watchlist or directly to the queue.
|
|
|
|
The page also includes:
|
|
|
|
- A live queue view that refreshes automatically.
|
|
|
|
## Config Page
|
|
|
|
The Config page at `/config` is where saved defaults now live.
|
|
|
|
Use it to:
|
|
|
|
1. Set the default download folder.
|
|
2. Choose the default `sub` or `dub` search mode.
|
|
3. Choose the default search quality used to prefill the Search page.
|
|
4. Enable or disable periodic watchlist refresh checks.
|
|
5. Set the refresh period in minutes for that scheduled watchlist job.
|
|
|
|
Those saved defaults are written into the project-local `config.json` and loaded automatically when the Search page opens.
|
|
|
|
The Config page also shows the current app version plus dependency status in a compact centered layout.
|
|
|
|
## Access Guard
|
|
|
|
By default, `ani-cli-web` only serves loopback clients such as `127.0.0.1` and `::1`.
|
|
|
|
If you intentionally want LAN or remote access, set:
|
|
|
|
```sh
|
|
ANI_CLI_WEB_ALLOW_REMOTE=1
|
|
ANI_CLI_WEB_REMOTE_TOKEN=choose-a-long-random-token
|
|
```
|
|
|
|
Non-local clients must then send either:
|
|
|
|
- `Authorization: Bearer <token>`
|
|
- `X-AniCli-Web-Token: <token>`
|
|
|
|
For browser access from another machine, you can also open the app once with:
|
|
|
|
```text
|
|
http://YOUR-HOST:8421/?token=your-token-here
|
|
```
|
|
|
|
The server will set an HTTP-only cookie and redirect to the same page without the token in the URL, so normal browser navigation and API polling continue to work afterward.
|
|
|
|
When the app is placed behind a loopback reverse proxy, forwarded external client IP headers such as `X-Forwarded-For` are also treated as non-local, so proxied internet or LAN traffic still needs the token.
|
|
|
|
This keeps the queue, config, and watchlist APIs safer when the app is intentionally exposed beyond localhost.
|
|
|
|
Malformed watchlist action payloads now return `400 Bad Request` when required JSON fields are missing, instead of surfacing those client mistakes as `404 Not Found`.
|
|
|
|
## Download Queue
|
|
|
|
The queue is stored in SQLite and shown 10 jobs at a time.
|
|
|
|
Core queue fields and logs are now stored in structured SQLite columns, while JSON payload storage is kept only for extra fields that do not have dedicated columns yet.
|
|
|
|
Queue actions currently available:
|
|
|
|
- `Retry` for finished or failed jobs.
|
|
- `Cancel` for pending or running jobs.
|
|
- `Remove` for completed or failed jobs.
|
|
- `Retry failed` for all failed jobs.
|
|
- `Remove finished` for completed jobs.
|
|
|
|
Downloads are staged inside the project-local app state first. After `ani-cli` finishes, files are moved into a Plex/Jellyfin-friendly layout:
|
|
|
|
```text
|
|
ANI_CLI_DOWNLOAD_DIR/anime_name/Season 01/anime_name - S01E01.mp4
|
|
```
|
|
|
|
## Watchlist
|
|
|
|
The watchlist stores one row per show in SQLite, keeps separate `sub` and `dub` episode counts, and assigns each show to one of four categories:
|
|
|
|
- `Watching`
|
|
- `Planned`
|
|
- `Finished`
|
|
- `Dropped`
|
|
|
|
You can add entries in two ways:
|
|
|
|
1. From the Search page with `Add to watchlist`, choosing the category first.
|
|
2. Manually on `/watchlist` with a title, AllAnime `show_id`, and category.
|
|
|
|
Watchlist features:
|
|
|
|
1. `Refresh` updates one tracked show.
|
|
2. `Refresh All` runs in the background, rejects duplicate queueing while a refresh is already pending or running, preserves interrupted job status across restarts, and refreshes shows in parallel without doing inline thumbnail downloads for every entry.
|
|
3. `Open` jumps back to the Search page with the title pre-filled.
|
|
4. Clicking the anime title opens the AllManga source page at `bangumi/<show_id>` in a new tab.
|
|
5. `Remove` deletes the watchlist entry and clears its cached poster file.
|
|
6. Poster cards use a compact multi-column grid with pagination at 30 cards per page.
|
|
7. Summary cards show tracked shows plus total `sub` and `dub` episode counts.
|
|
8. Episode pills show `current / total` progress when AnimeSchedule exposes the planned episode count.
|
|
9. `Sub` and `Dub` pills turn green and gain `Sub ready` or `Dub ready` tags when that release track appears fully finished and ready for download.
|
|
10. Each category has its own tab on the watchlist page.
|
|
11. Every card includes an inline category selector so you can move tracked anime between tabs at any time.
|
|
12. Finished shows display a small check badge: green means the anime was downloaded through the app, gray means it was marked finished manually.
|
|
|
|
### Download Sync
|
|
|
|
Successful downloads now feed back into the watchlist automatically.
|
|
|
|
Current behavior:
|
|
|
|
- If the anime is already on the watchlist, a successful download marks it as downloaded and keeps its current category.
|
|
- If the anime is not yet on the watchlist, `ani-cli-web` creates a new watchlist entry in `Finished` after the download succeeds.
|
|
- Those add and download-sync paths now queue a background refresh for episode counts, AnimeSchedule metadata, and thumbnails instead of waiting for all upstream fetches before returning.
|
|
- If a queued job ever reaches the older search-based fallback path for `show_id` resolution, the app now only accepts that fallback when the queued title still matches the returned result confidently, preferring to fail safe over marking the wrong anime as downloaded.
|
|
- That fallback now also preserves season and part distinctions, so `Season 2` downloads do not silently sync against a different season with the same base title.
|
|
- When that safer fallback still cannot resolve a `show_id`, the failure now keeps the specific reason so logs and error messages are easier to diagnose.
|
|
- Existing watchlist entries from older versions are migrated into the `Watching` category automatically.
|
|
|
|
### Thumbnail Behavior
|
|
|
|
Watchlist thumbnails use the local `/api/watchlist/thumb/...` route, not direct remote image URLs.
|
|
|
|
Current thumbnail behavior:
|
|
|
|
- Cached thumbnails render from the local project cache when available.
|
|
- The page adds a fresh nonce to thumbnail URLs to avoid stale browser-cached misses.
|
|
- Missing visible covers are warmed up in small batches instead of one request per card all at once.
|
|
- The thumbnail warm-up API now requires `show_ids` to be a JSON array and accepts explicit boolean-style `force` values such as `true` and `false`.
|
|
- The page only retries an unresolved cover warm-up once per thumbnail state change during the current browser session, so silent watchlist polling does not keep hammering the same missing poster.
|
|
- If a show disappears while a thumbnail warm-up batch is running, that entry is skipped cleanly instead of failing the whole batch request.
|
|
- Thumbnail image tags only hit the local route for already-cached files, so opening the watchlist no longer triggers a per-card remote lookup burst.
|
|
- AnimeSchedule is tried first for cover lookup.
|
|
- Stale stored AnimeSchedule routes and AniDB IDs now fall back to fresh title-based lookups instead of aborting the whole thumbnail refresh attempt.
|
|
- If a season-labeled title fails as-is, lookup retries with simplified base-title queries.
|
|
- AniDB is used as a fallback when AnimeSchedule does not resolve a cover.
|
|
- AniDB fallback reuses the cached parsed title list directly to keep repeated lookups lighter.
|
|
- Failed automatic fetch attempts now start the retry window, so bad lookups back off instead of hammering AnimeSchedule and AniDB on every warm-up pass.
|
|
|
|
### Watchlist Pagination
|
|
|
|
Watchlist listing responses now clamp out-of-range page requests to the last available page before querying rows, so the returned `page` number and `items` stay consistent even when the browser or API client asks for a page past the end.
|
|
|
|
Per-card thumbnail tools:
|
|
|
|
1. `Reload` forces a fresh redownload attempt for that show's poster.
|
|
2. `Upload` appears when no thumbnail is currently available and lets you assign a local image manually.
|
|
3. Hovering the poster tile or card shows the current thumbnail source hint.
|
|
|
|
Manual uploads are saved into the same local thumbnail cache used by automatic cover downloads.
|
|
|
|
### Completion Detection
|
|
|
|
Watchlist refresh still uses AllAnime, the same upstream source that `ani-cli` uses, to count which `sub` and `dub` episodes are actually available right now.
|
|
|
|
To decide whether a release track looks complete, `ani-cli-web` also checks AnimeSchedule for:
|
|
|
|
- the anime's expected total episode count
|
|
- the upstream airing status such as `Finished` or `Ongoing`
|
|
|
|
When AnimeSchedule reports a known total and `Finished`, the watchlist compares that total with the live AllAnime `sub` and `dub` counts:
|
|
|
|
- `Sub` becomes ready when available sub episodes reach the expected total.
|
|
- `Dub` becomes ready when available dub episodes reach the expected total.
|
|
|
|
If AnimeSchedule does not know the total yet, the watchlist keeps showing the plain available-count value without a completion highlight.
|
|
|
|
## Runtime State
|
|
|
|
All runtime state is stored under:
|
|
|
|
```text
|
|
ani-cli-web/.ani-cli-web/
|
|
```
|
|
|
|
That folder currently holds:
|
|
|
|
- `config.json`
|
|
- `state.sqlite3`
|
|
- legacy `queue.json` migration input when present
|
|
- legacy `watchlist.json` migration input when present
|
|
- `thumbnails/`
|
|
- AniDB title cache data
|
|
- staging files for active downloads
|
|
|
|
Legacy files from `~/.config/ani-cli-web/` and `~/.local/state/ani-cli-web/` are migrated into the project-local folder automatically on startup.
|
|
The earlier project-local `queue.sqlite3` filename is also migrated automatically into `state.sqlite3`.
|
|
|
|
## Configuration
|
|
|
|
Environment variables:
|
|
|
|
- `ANI_CLI_BIN`: path or command name for `ani-cli`; defaults to `ani-cli` from `PATH`.
|
|
- `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 when set to `1`, `true`, `yes`, or `on`.
|
|
- `ANI_CLI_WEB_REMOTE_TOKEN`: required shared secret for non-loopback clients when remote access is enabled.
|
|
- `ANI_CLI_WEB_DEBUG`: enable debug logging when set to `1`, `true`, `yes`, or `on`.
|
|
- `ANI_CLI_WEB_STATE_ROOT`: override the project-local state directory path; useful for isolated test runs or custom storage locations.
|
|
- `UPDATE_ON_START`: when set to `1`, `true`, `yes`, or `on` in the container, runs `sudo ani-cli --update` before `ani-cli-web` starts.
|
|
- `USER_UID` and `USER_GID`: optional numeric UID/GID pair for running the container process as a specific host user/group so mounted files are created with that ownership.
|
|
|
|
Saved defaults from the UI are written into the project-local `config.json`.
|
|
|
|
The saved config now also includes `watchlist_auto_refresh_enabled` and `watchlist_auto_refresh_minutes`, which control the periodic background watchlist refresh schedule.
|
|
|
|
## Notes
|
|
|
|
- Importing `app.py` no longer boots the runtime worker threads automatically; runtime setup is deferred until startup or first use.
|
|
- Importing `app_support.py` no longer migrates or creates runtime state on its own; migrations now happen during runtime bootstrap.
|
|
- Cheap routes such as the page shells, `/api/version`, `/api/dependencies`, and `/api/config` do not need to boot the full runtime before responding.
|
|
- The CLI startup path itself also stays lazy now; runtime bootstrap happens when the first runtime-backed request arrives.
|
|
- Watchlist and queue data are stored in the same project-local SQLite database file, `state.sqlite3`.
|
|
- Runtime folders such as `.ani-cli-web/`, `downloads/`, and Python bytecode cache directories at any depth are ignored by git.
|
|
- The Docker image clones both upstream repositories during build, so the app source inside the container comes from the configured git remotes rather than the local build context.
|
|
- Focused regression tests live in `test_app.py`, run against an isolated temporary state root, and can be started with `python3 -m unittest test_app.py`.
|