I Love Talking to My Server, But It Has No Sense of Restraint
I have a local bot named Archie that handles media requests. I ask it for a show through Telegram, confirm the search result, and it follows the download through Sonarr. That part took an afternoon. Language models are good at language, and "add the new season of that show" is language.
The hard part came after. A conversational interface makes it frictionless to add things, and frictionless is exactly what you don't want pointed at a finite disk. Every "sure, add it" is a few gigabytes you didn't think about. Multiply by twenty episodes, then several seasons, then a year of casual requests, and you have a full array and no memory of how it happened.
So this post isn't really about media. It's about three decisions I had to get right before I'd let a model touch anything that consumes a resource: what credentials it holds, where policy lives, and how it knows an action actually happened.
One scoping note up front: finding releases is Sonarr's job, and I'm not covering indexer configuration here. That's an exercise for the reader. This post starts at the point where you already have a working library and want to put a conversational front end on it.
What the Pipeline Actually Looks Like
The important design choice is that Archie is not a download client and it does not choose releases. It translates a request into a small, structured command and hands it off.
graph LR
subgraph "Request path"
A[Telegram] --> B[Archie]
B --> C[arr-api proxy]
C --> D[Sonarr]
D --> E[Download client]
end
subgraph "Policy"
D --> F[Quality profile
chooses releases]
end
Each box owns exactly one decision. The bot handles language and confirmation. The proxy defines its authority. Sonarr tracks the library and evaluates releases. The download client moves bytes.
That separation is what makes the system reasonable to think about. When something goes wrong, there's one place to look, and none of those places is "the model decided something weird."
Give the Bot Capabilities, Not Credentials
Sonarr requires an API key. Putting that key in Archie's environment would be the obvious move, and it would be a mistake.
Here's why: the bot has a terminal tool. Anything in its environment is readable by anything the model can be talked into running. A prompt injection buried in a show description (or a filename, or anything else that makes it into context) could ask the bot to print its environment or send the credential somewhere else. The key isn't just a key. It's every capability that key grants, forever, to whoever gets it.
So Archie doesn't get the key. It gets an arr-api command that talks over a local Unix socket. A separate systemd service owns the real Sonarr, Radarr, and Lidarr credentials, and it exposes a deliberately narrow vocabulary:
arr-api tv search "breaking bad"
arr-api tv add 81189
arr-api tv verify 81189
arr-api tv queue
Archie can request an operation. It cannot read the credential used to perform it, and it cannot invent an API call the proxy doesn't support. If the model gets creative, the worst case is a rejected command.
Why I'm obsessed with this pattern: it generalizes to every LLM tool you will ever build. Expose the capability the model needs, not the secret that grants every capability. The blast radius of a compromised bot becomes the vocabulary you wrote, instead of everything the key can reach.
The Unexpected Answer Was SD
Now the disk problem. I assumed standard definition would feel like an obvious compromise. In practice, it works for most of the television I actually want to keep: a sitcom, an older series, a casual rewatch, anything playing on an ordinary screen while I do something else.
What surprised me was the arithmetic. A modest per-episode difference looks like nothing until you multiply it by twenty episodes, then several seasons, then an entire library. My first space-saving default was 720p, which sounded restrained next to 1080p and 4K. Complete seasons still added up fast.
That doesn't make HD pointless. A visually rich favourite, a modern show with real cinematography, anything watched on a large display, those may well be worth the space, and if storage is plentiful the whole trade changes. The useful shift for me was making higher quality the intentional exception instead of the automatic default.
Quality Has to Be a Policy, Not a Prompt Adjective
My first instinct was to tell the bot to find a "smaller" copy. This does not work, and it's worth being precise about why.
Words like standard, good, and small are ambiguous. Worse, the model never sees the full release metadata anyway, so it isn't in a position to evaluate the tradeoff even if the adjective were unambiguous. You are asking it to enforce a policy using information it does not have.
The enforceable control is Sonarr's quality profile. When Archie adds a show, the proxy fetches the available profiles and selects the first match in this order:
SDStandardHD-720pHD - 720p/1080p- Stop with an error if none exists
In the generated request, that choice becomes qualityProfileId:
{
"title": "Example Show",
"tvdbId": 12345,
"qualityProfileId": 3,
"monitored": true,
"addOptions": {
"searchForMissingEpisodes": true
}
}
The ordering matters, and so does the last line. SD is the storage-saving target. The two HD entries are capped fallbacks for an install where the SD profile was renamed or removed. The code deliberately does not fall back to the first profile in the list, or to Any, because either choice could quietly admit 1080p or 4K and turn a missing configuration into an expensive surprise. Failing loudly is the correct behaviour when the policy can't be found.
The nice consequence: "SD" isn't a hard-coded resolution living in the bot. It's a reference to a policy maintained in Sonarr. Allowed sources, formats, upgrade rules, cutoffs, and size limits all still come from that profile. When I want tighter limits, I change the profile. I don't try to teach a language model a new interpretation of "small."
The Trap: Assignment Happens at Add Time
Here's the operational gotcha I walked into. Changing the bot's default only affects shows added after the new configuration is deployed. Sonarr stores the selected profile on each series. Everything already in the library keeps its old 720p or 1080p profile and will keep using it for future episode searches.
So the rollout is two separate actions, not one:
- Step 1 of 2: Deploy the new proxy so future additions get the SD profile.
- Step 2 of 2: In Sonarr, bulk-edit the existing series that should also move to SD.
Keeping that migration explicit is safer than silently rewriting every series during a deploy. A few shows genuinely deserve a higher-quality profile, and a configuration deployment should not erase that decision without review.
Verify the Mutation, Don't Trust the Request
After Archie sends the add request, the proxy reads the series back from Sonarr by TVDB ID. It reports success only if that read returns the show.
An accepted HTTP request is not proof that the intended state exists. This is true of all software, but it matters more with a model in the loop, because models are eager to turn "I ran the command" into "the download was added." Those are different claims and the gap between them is where you lose an evening.
So the tool returns a concrete verified result, and the prompt requires Archie to distinguish between added, queued, downloading, and actually present on disk. Four different states, four different answers, no blurring.
Why This Matters Beyond Media
The quality-profile lesson and the verification lesson are the same lesson: move policy and truth checks into deterministic code.
Let the model decide which well-bounded action matches the conversation. That's the thing it's genuinely good at: turning messy human intent into one of a handful of structured options. Then let ordinary software enforce what that action actually means.
The conversational interface is a front end. It should not also be the policy engine, the credential store, or the source of truth. Once I stopped asking it to be those things, the bot got both more useful and considerably less alarming.
And the disk stopped filling up.
Header photo by Vadim Bogulov on Unsplash.
Content on this blog was created using human and AI-assisted workflows described in my standards and workflow posts. Original ideas and editorial decisions by Justin Quaintance.