Your download finished. FileBot knows what to do. This UI makes sure you never have to open a terminal to tell it.
Self-hosted web UI that connects Deluge + FileBot + Plex into a single workflow — pick completed torrents, rename and move them to your media library, delete the torrent, refresh Plex. One click.
- Login via Plex PIN OAuth (single-user, no local accounts)
- View completed torrents fetched live from Deluge
- Select one or more finished torrents
- Configure FileBot — DB, format, action, conflict resolution
- Execute — media engine validates request, renames and moves files to
MEDIA_ROOT - On move success (per torrent) — each successfully moved torrent is deleted from Deluge; failed torrents are kept
- See results — per-file success/failure, per-torrent moved/deleted status, raw FileBot output toggle
- No directory browser — source always comes from Deluge download path
- No output path picker — destination is always
MEDIA_ROOT(locked down for security) - No multi-user — single Plex account owns the instance
- No multi-Deluge-server UI — one Deluge via env vars
- No Plex library browser — only triggers refresh after move
If you want a full-featured dashboard, this is not it. This is a surgical tool.
See doc/architecture.md — Rename/Move Execution Flow for the full sequence diagram.
Login (Plex PIN)
→ completed torrents list (polls Deluge every 30s)
→ select torrents → FileBot form
→ POST /filebot/execute
→ validate allowlist
→ resolve metadata (TMDB / AniDB)
→ rename + move to MEDIA_ROOT
→ delete each moved torrent from Deluge
→ refresh Plex
→ result page (per-file OK/ERR · per-torrent status)
cp .env.example .env
# edit .env — set JWT_SECRET, PLEX_*, DELUGE_*, MEDIA_ROOT
docker compose up -dOpen http://localhost:8080.
services:
filebot-webui:
image: darknessnerd/filebot-webui:latest
ports:
- "8080:8080"
environment:
JWT_SECRET: change-me-long-random-string
PLEX_CLIENT_ID: filebot-webui
PLEX_REDIRECT_URL: http://your-host:8080/auth/plex/forward
DELUGE_HOST: 192.168.1.x
DELUGE_PORT: 8112
DELUGE_PASSWORD: your-password
MEDIA_ROOT: /media
TMDB_ACCESS_TOKEN: your-tmdb-bearer-token
volumes:
- /data/downloads:/downloads # must match Deluge's download path
- /data/media:/media
restart: unless-stoppedNo external binaries required — media renaming is handled natively via the TMDB API. Set
TMDB_ACCESS_TOKENto a TMDB v4 bearer token (read-only access token from your TMDB account settings).
Critical: Deluge and filebot-webui must mount the same paths at the same container paths. If Deluge stores files at
/downloads/Movie.mkv, filebot-webui must also see/downloads/Movie.mkv. Mismatch = file not found.
# prereqs: Go 1.24+, filebot binary installed on host (or stub), Deluge running somewhere
# note: Docker image bundles FileBot — local dev requires it separately
cp .env.example .env
# edit .env — minimum: JWT_SECRET, MEDIA_ROOT, DELUGE_HOST, DELUGE_PASSWORD
mkdir -p data
# option A: hot reload (recommended)
go install github.com/air-verse/air@latest
air
# option B: plain run
make run
# option C: manual
go run ./cmd/webui-bemake test # go test ./...
make test-race # go test -race ./...
make lint # go vet + staticcheck| Variable | Description |
|---|---|
JWT_SECRET |
JWT signing secret — long random string, no default, app refuses to start without it |
MEDIA_ROOT |
Absolute path FileBot moves files into — all output paths validated against this |
PLEX_CLIENT_ID |
Plex app name / client identifier |
PLEX_REDIRECT_URL |
Full URL of /auth/plex/forward on this app |
DELUGE_HOST |
Deluge daemon hostname or IP |
DELUGE_PASSWORD |
Deluge web UI password |
| Variable | Default | Description |
|---|---|---|
SERVER_HOST |
0.0.0.0 |
Bind address |
SERVER_PORT |
8080 |
HTTP port |
LOG_LEVEL |
info |
debug, info, warn, error |
DEBUG |
false |
Pretty-print logs |
DEV_MODE |
false |
Bypass auth and mock Deluge/Plex — metadata resolvers (TMDB, AniDB, scheduler) stay real |
JWT_EXPIRES_IN |
24h |
JWT token lifetime |
JWT_ISSUER |
filebot-webui |
JWT issuer claim |
DELUGE_PORT |
8112 |
Deluge JSON-RPC port |
DELUGE_USERNAME |
(empty) | Deluge username (if required) |
TMDB_ACCESS_TOKEN |
(empty) | TMDB v4 bearer token (required only when using TheMovieDB / TheMovieDB::TV) |
ANIDB_CLIENT |
(empty) | Registered AniDB HTTP API client id (required for AniDB lookups) |
ANIDB_CLIENTVER |
(empty) | Registered AniDB client version (required for AniDB lookups) |
ANIDB_PROTOVER |
1 |
AniDB HTTP API protocol version |
ANIDB_BASE_URL |
http://api.anidb.net:9001/httpapi |
AniDB HTTP API endpoint |
ANIDB_TITLES_FILE |
(empty) | Optional local AniDB titles XML file for title→AID resolution |
ANIDB_TITLES_URL |
(empty) | Optional URL to download AniDB titles XML from |
ANIDB_REFRESH_TITLES_ON_START |
false |
When true, refreshes ANIDB_TITLES_FILE at startup |
ANIDB_SCHEDULER_ENABLED |
false |
When true (and ANIDB_TITLES_URL + ANIDB_TITLES_FILE set), runs a background scheduler that keeps the titles file up to date |
ANIDB_SCHEDULER_INTERVAL |
12h |
How often the scheduler wakes up to check whether a remote download is needed |
ANIDB_MIN_FETCH_INTERVAL |
24h |
Minimum age of the local file before a remote download is triggered (AniDB policy: ≥ 24h) |
DB_TYPE |
sqlite |
sqlite or postgres |
DB_DATABASE |
./data/app.db |
SQLite path or PostgreSQL DB name |
DB_HOST |
localhost |
PostgreSQL host |
DB_PORT |
5432 |
PostgreSQL port |
DB_USERNAME |
(empty) | PostgreSQL user |
DB_PASSWORD |
(empty) | PostgreSQL password |
DB_SSL_MODE |
disable |
PostgreSQL SSL mode |
All values validated against an allowlist before execution. Raw user input is never interpolated into commands.
| Parameter | UI control | Allowed values |
|---|---|---|
--db |
dropdown | TheMovieDB, TheMovieDB::TV, AniDB |
--action |
dropdown | move, copy, symlink, hardlink, test |
--conflict |
dropdown | skip, replace, auto, index, fail |
--filter |
text input | optional Groovy expression |
--q |
text input | override query (aid:<id> preferred for AniDB; title query requires ANIDB_TITLES_FILE) |
-r |
checkbox | recursive mode |
| DB | Output layout |
|---|---|
TheMovieDB |
Movies/<Title (Year)>/<Title>.<ext> |
TheMovieDB::TV |
TV/<Show>/Season N/<Show> - SxxEyy.<ext> |
AniDB |
Anime/<Title>/Season N/<Title> - SxxEyy.<ext> |
TV — episode marker extracted from filename, in priority order:
| Format | Example |
|---|---|
SxxEyy / SxxEyyEzz / SxxEyy-Ezz |
Show.S01E01.mkv, S01E01-E02 |
S01.E01 / S01 E01 |
Show.S01.E01.mkv |
NxYY |
Show.2x05.mkv |
Movie — year extracted from filename; title normalized by stripping codec noise (1080p, BluRay, x265, DTS, …), language tags, release group brackets, and audio channel specs.
Anime — episode marker extracted in priority order:
| Format | Example |
|---|---|
SxxEyy / NxYY |
Show.S02E04.mkv |
EP01 / E01 / E01-E13 |
[Group] Show - E01.mkv |
[N/Total] / [N-M/Total] |
Show [03/14].mkv |
- 01 / - 01-26 (bare after dash) |
[SubsPlease] Show - 01 (1080p).mkv |
#01 / #01-05 |
Show #01.mkv |
OVA N / SP N / Special N |
Hellsing OVA 3.mkv |
Part N / Part III |
Tenchi Muyo Part II.mkv |
Season inferred from Season N / Stagione N in path; defaults to 1.
--q aid:<id>— deterministic direct lookup (recommended)- Title-based lookup when
ANIDB_TITLES_FILEconfigured; title cleaned of episode markers before index search - Ambiguous titles (same text mapping to multiple AIDs) are narrowed by title type (
main>official> other), then by release year if still tied; unresolved ambiguity errors with anaid:<id>hint - Startup refresh when
ANIDB_REFRESH_TITLES_ON_START=true,ANIDB_TITLES_URL, andANIDB_TITLES_FILEset - Background scheduler when
ANIDB_SCHEDULER_ENABLED=true,ANIDB_TITLES_URL, andANIDB_TITLES_FILEset (see below)
--filterunsupported in native engine
The scheduler keeps the local AniDB titles file (ANIDB_TITLES_FILE) up to date in the background, without manual intervention.
- The scheduler wakes every
ANIDB_SCHEDULER_INTERVAL(default12h). - On each tick it checks the modification time of
ANIDB_TITLES_FILE. - A remote download is performed only when the file is older than
ANIDB_MIN_FETCH_INTERVAL(default24h). - If a download is already running (e.g. a very slow network), the next tick is skipped — no concurrent downloads.
- The scheduler shuts down cleanly when the process receives
SIGINTorSIGTERM.
AniDB requires that the anime-titles dump is not fetched more than once per 24 hours.
This is enforced by the ANIDB_MIN_FETCH_INTERVAL setting (default 24h).
Do not set ANIDB_MIN_FETCH_INTERVAL below 24h — doing so violates AniDB's terms of service and may result in your IP being banned.
ANIDB_TITLES_FILE=./data/anime-titles.xml
ANIDB_TITLES_URL=https://anidb.net/api/anime-titles.xml.gz
ANIDB_SCHEDULER_ENABLED=true
ANIDB_SCHEDULER_INTERVAL=12h
ANIDB_MIN_FETCH_INTERVAL=24hSet ANIDB_REFRESH_TITLES_ON_START=true alongside to also populate the file immediately at first boot (before the first scheduler tick fires).
Clean layered architecture — strict import direction enforced, no shortcuts:
handler → service → domain ← repository
| Package | Role |
|---|---|
internal/domain/ |
Value types + sentinel errors — zero external imports |
internal/service/ |
Business logic — imports domain only, defines its own interfaces |
internal/repository/ |
DB access — implements service interfaces implicitly |
internal/handler/ |
HTTP — imports service only via local interfaces |
cmd/webui-be/main.go |
Wires all layers — only file allowed to import across them |
See doc/architecture.md for:
- C4 container diagram (external integrations, owned boundary)
- Rename/Move execution sequence diagram
- AniDB titles scheduler sequence diagram
┌─────────────────────────────────────────────────────────────────┐
│ Browser (HTMX + Alpine.js) │
└────────────────────────┬────────────────────────────────────────┘
│ HTTP
┌────────────────────────▼────────────────────────────────────────┐
│ handler/ │
│ ├── AuthHandler Plex PIN OAuth · JWT cookie │
│ ├── TorrentHandler list completed Deluge torrents │
│ ├── FileBotHandler form + execute + result │
│ └── PlexHandler library refresh │
└────────┬───────────────┬──────────────────┬─────────────────────┘
│ │ │
┌────────▼──────┐ ┌──────▼──────┐ ┌────────▼────────────────────┐
│ service/auth │ │service/deluge│ │ service/filebot │
│ JWT · Plex │ │ JSON-RPC │ │ InternalEngine │
│ PIN flow │ └──────┬──────┘ │ ├── MovieResolver (TMDB) │
└───────┬───────┘ │ │ ├── TVResolver (TMDB) │
│ Deluge RPC │ └── AnimeResolver (AniDB) │
┌───────▼───────┐ │ └────────┬────────────────────-┘
│ repository/ │ │ │
│ UserRepo │ │ ┌─────────▼──────────────────────┐
│ (SQLite/PG) │ │ │ service/anidb │
└───────────────┘ │ │ Client · title index (RWMutex)│
│ │ Scheduler (background goroutine│
│ └─────────┬──────────────────────┘
│ │
Deluge host AniDB HTTP API
+ titles dump
| Layer | Technology |
|---|---|
| Language | Go 1.24 |
| HTTP | net/http stdlib (Go 1.22 ServeMux) |
| Frontend | HTMX + Alpine.js |
| Templates | html/template stdlib, embedded via embed.FS |
| Auth | Plex PIN OAuth + JWT HS256 (HttpOnly cookie) |
| Database | SQLite (default) / PostgreSQL |
| Logging | zerolog behind injected interface |
| Media engine | Native Go implementation; TMDB and AniDB metadata lookups via HTTP APIs |
- FileBot service validates args before execution; CLI adapter never receives raw shell input
source_pathsvalidated against shell metacharacters before exec — Deluge paths containing$,`,;,|etc. are rejected--outputvalidated to be underMEDIA_ROOTviafilepath.Cleanwith separator guard (prevents/media2bypassing a/mediaprefix check)JWT_SECRETrequired at startup — no insecure fallback- JWT lives in HttpOnly cookie only — never in URL params or localStorage
- Parameterized SQL queries throughout
Securecookie flag auto-enabled behind HTTPS /X-Forwarded-Protoproxy
git tag v1.0.0
git push origin v1.0.0CI builds and pushes :1.0.0, :1.0, and :latest to Docker Hub.
| Symptom | Cause | Fix |
|---|---|---|
JWT_SECRET is required at startup |
Env var missing | Set JWT_SECRET |
MEDIA_ROOT is required at startup |
Env var missing | Set MEDIA_ROOT |
| TMDB database selected but no token | TMDB_ACCESS_TOKEN not set |
Set TMDB_ACCESS_TOKEN to a TMDB v4 bearer token — required only for TheMovieDB / TheMovieDB::TV |
| Torrents not showing | Deluge unreachable | Check DELUGE_HOST, DELUGE_PORT, DELUGE_PASSWORD |
| Media rename "file not found" | Volume mount mismatch | Mirror paths between Deluge and filebot-webui containers |
| Media rename "outside MEDIA_ROOT" | Output path rejected | Ensure --output is under MEDIA_ROOT |
| Login loop | JWT cookie not set | Check PLEX_REDIRECT_URL matches actual app URL exactly |
| Plex refresh fails | Server unreachable or wrong token | Check Plex server reachable from container network |
connection refused on port 8112 |
DELUGE_HOST empty |
Set DELUGE_HOST to your Deluge machine IP/hostname |