Developing and running it¶
This is the technical side, for me.
The files¶
| Path | What it is |
|---|---|
borer/main.py |
the service: the page's server, the JSON API, and the commands below |
borer/worker.py |
the searching, grabbing, following and checking, in two threads (the passes every 10 minutes, and the guard every minute) |
borer/rules.py |
what's thrown out, the scores, and when it's sure |
borer/checks.py |
the look inside a torrent's file list, and ffprobe on a file that arrived |
borer/seeding.py |
the seeding tiers, and what the library also has |
borer/auth.py |
the password for borer.tiresia.xyz |
borer/arr.py |
talking to Radarr, Sonarr, Seerr and Transmission |
borer/state.py |
its memory, one JSON file |
app/ |
the page: index.html, style.css, app.js and the icon |
tests/ |
the tests, and the real searches they run on |
tools/demo.py |
the page over made-up data, for screenshots |
borer.service |
the user service, as a template |
Everything is Python's standard library, plus ffprobe for the checks.
Running it¶
It's a user service. The borer.service in the repo is a template: copy it, then set the paths and addresses for the machine. On my server the installed copy runs from /srv/dev/borer, listens on the tailnet address too, and sends the email through my notify.sh.
cp borer.service ~/.config/systemd/user/ && systemctl --user daemon-reload && systemctl --user enable --now borer
systemctl --user restart borer # after a change
journalctl --user -u borer -f # what it's doing
The service's settings are environment variables in borer.service:
| Variable | What it is |
|---|---|
BORER_STATE |
the state folder, /srv/docker/borer |
BORER_PORT, BORER_HOSTS |
where the page listens: port 8500, on 127.0.0.1 and any other addresses after a comma (mine adds the tailnet one) |
BORER_URL |
the page's address in the morning email |
BORER_SEARCHES |
searches per pass (4) |
BORER_ALLOW |
extra networks let in, for screenshots from a container |
BORER_NOTIFY |
the script that sends the morning email lines, called with a title and a body. Without it, no email |
The other commands:
python3 borer/main.py passruns one pass in the foreground, then exits.python3 borer/main.py dryruns one pass with nothing grabbed and nothing saved to the real state (it writesstate-dry.json), and prints what it would do.python3 borer/main.py pageserves the page over that dry run, with nothing running behind it.
The address and the password¶
https://borer.tiresia.xyz is a public hostname on the morten Cloudflare tunnel, pointing at http://localhost:8500. The tunnel is managed in the Cloudflare dashboard (Zero Trust › Networks › Tunnels), not in /etc/cloudflared.
The tunnel connects from this machine, so Borer tells a tunnel request by its headers: anything with Cf-Connecting-Ip, X-Forwarded-For, X-Forwarded-Host or Forwarded needs the password, and anything straight from 127.0.0.1 or the tailnet without them gets in. Anything from anywhere else gets a 403, not even the sign-in page.
The password¶
python3 /srv/dev/borer/borer/main.py set-password
It asks twice, and changing it signs out every browser. The first time the service starts it makes a random one in /srv/docker/borer/initial-password.txt (only I can read it), and set-password deletes that file.
- It's kept as a scrypt hash in
/srv/docker/borer/auth.json, with the secret that signs the cookies. - The cookie,
borer, lasts a year, and it'sHttpOnly,SameSite=LaxandSecureover https. - Eight wrong passwords from one address in 15 minutes, or 30 from everywhere in an hour, and it says "Too many tries" until that clears. Three wrong ones from an address go in the page's log, and so does every sign-in.
- Every answer is
private, no-store, so Cloudflare never keeps a copy, andX-Frame-Options: DENY, so no other site can put the page in a frame. - The page's actions only take JSON, so a form on some other site can't send one.
The state¶
Its memory is one file, /srv/docker/borer/state.json, written whole and swapped in, so a crash never leaves half of it. It holds the items (units), the grabs, what was thrown out, the trusted groups, the settings, the seeding entries and the log.
The keys come from Radarr's, Sonarr's and Seerr's own config files, and Transmission's login from /srv/docker/.env, every time it starts, so a new key only needs a restart. Nothing secret lives in this repo.
The tests¶
python3 -m unittest discover -s tests
They run on real searches I saved on 2 Nov 2026 (Sense and Sensibility, Hope and Animal Control), plus the seeding rules, the stall clock, a film removed from Radarr while it downloads, the Settings edits, and the password on a spare port.
The docs¶
These pages are in docs/, and docs.tiresia.xyz builds them within 10 minutes of a push to main.
The pictures come from python3 tools/demo.py, which serves the page over made-up data: public-domain films and shows, made-up release groups and a made-up "Sam", with nothing running behind it. Pictures of my own Borer would show what I've downloaded and who asked for it, so they never go in here.