Skip to content

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 pass runs one pass in the foreground, then exits.
  • python3 borer/main.py dry runs one pass with nothing grabbed and nothing saved to the real state (it writes state-dry.json), and prints what it would do.
  • python3 borer/main.py page serves 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's HttpOnly, SameSite=Lax and Secure over 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, and X-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.