Skip to content
← All projects
automation production

Texting my home server

A Telegram bot with natural-language library search and AI recommendations.

24/7 media library, driven from a chat window
  • Python
  • Telegram Bot API
  • Claude API

Where this could work for other businesses

Strip away the media-server framing and what’s left is a simple mechanism: put a chat window in front of a catalogue, let people search it in plain English, and route the reply only to whoever actually cares. That’s a much more general tool than “add a film to Plex.”

A small bookshop or record store could run the exact same thing over their own stock instead of a media library — a customer texts “anything like a slow-burn Scandi crime novel” and gets a real answer grounded in what’s actually on the shelf, not a generic “you may also like” widget bolted onto a website nobody visits.

Internal IT and facilities requests have the identical shape to the original problem this solved: someone messaging a person instead of a system because the system is worse than a person. Point the same “text it, it searches, it asks before it acts” pattern at equipment or access requests, and the approval gate that stops the bot deleting a show unsupervised is the same gate that stops it provisioning software unsupervised.

Community-run catalogues — a tool library, a board-game café, a makerspace’s shared equipment list — have the same discovery problem at smaller scale: nobody remembers what’s in the collection, and a web form nobody built properly isn’t going to fix that. A chat-based search over the actual catalogue would.

And the personalised-notification piece generalises on its own: any business that currently blasts one announcement to an entire list — new stock in, a table opened up, a slot became available — could instead ask “who does this specific thing actually matter to” before sending anything, the same way this bot decided who’d care about a new arrival instead of telling the whole household.

The problem

I was sick of getting a WhatsApp message at half eleven at night asking me to add Citizen Kane to Plex. Not because anyone wanted to watch it right then — because the web interface for the media server lives on the local network, requires a login, and generally has the user experience of a filing cabinet. So instead of doing it themselves, people just texted me. Which meant nothing got added unless I happened to be awake and in the mood, and things sat unwatched for months because nobody was tracking whether the request had ever actually been fulfilled.

Eventually I thought: sod it, let’s automate this so I can get some sleep.

There was a smaller, quieter problem underneath the annoying one. With a few hundred shows and films sitting on the server, nobody — including me — could remember what was actually in there. “Have we got anything like a hard-boiled detective show?” always came back to me, because none of the standard tools do vibes-based search.

The goal

Replace “message the guy who runs the server” with “message the server.” If adding something is as easy as texting a bot, the whole delegation problem disappears, and if the bot can also answer “what have we got that’s like X” in plain English, so does the discovery problem. The bar was low: anything that meant fewer messages landing on my phone at bedtime counted as a win.

What it does

You text the bot a show name — say, Dragnet — and it gets added. You text Adventures of Robin Hood S03E07 and it adds just that one episode, no more, no less. If a name matches more than one result, it lists a few options and asks which one, like a slightly less patient version of me. The whole command surface is a plain-text menu — no app, no login, just a list of things you can type:

The bot's help menu, listing commands for adding and removing shows and movies, plus recent activity, storage, watch history, recommendations, natural-language search, cleanup, and live streaming status. The full command list — the entire interface is this menu and a text box.

Once a week it goes looking for anything that’s sat unwatched for months and asks about it one item at a time — keep it, or bin it, with a proper button for each, because I’m not trusting a text reply of “yes” to mean the right thing at the right moment. Ask it for recommendations and it looks at what you’ve actually watched — Richard Diamond and Philip Marlowe, say — and suggests five things you’d probably like, each with an actual reason, not a “people who watched this also watched” shrug:

Four recommendation cards: Chinatown, The Fall, the Three Colors Trilogy, and a Godard documentary, each with a short reason tied to specific viewing history. Recommendations come with an actual reason, not a generic “people also liked.”

Describe what you’re in the mood for (“something like Martin Kane but a bit more downbeat”) and it searches the whole library for you in plain English. And when something new finishes downloading, instead of one “new content!!” message blasted to the whole household, whoever’s actually likely to care gets a message explaining why — everyone else gets left alone, which is the correct behaviour.

Under the bonnet it’s one small, boring service with no database to speak of — small enough that I can still read the whole thing in one sitting, which matters when it’s the only thing standing between the household and a very cross group chat.

The build

Built with Claude Code, with one rule I wasn’t willing to bend: the AI is allowed to have opinions, but it isn’t allowed to do anything. It can say “I reckon Bob would like this” or “this matches your search” — it never gets to press delete. Every suggestion is shown before anything happens, and anything destructive needs an actual tap on an actual button. I don’t trust a language model enough to let it near a delete key unsupervised, and I’d be suspicious of anyone who says they do.

Responses come back in a fixed, boring, predictable shape so a weird reply degrades gracefully instead of taking the whole bot down with it — schema-constrained output, not a paragraph the code then has to guess the meaning of. And I deliberately reached for the cheap, fast model rather than the clever one — this is “pick five things from a list” work, not “solve a hard problem” work, and nobody wants to wait four extra seconds for a recommendation to feel smart.

The natural-language search is really two smaller problems stacked on top of each other: turning a vague phrase into something that can be matched against a library, and then turning a handful of raw matches into something worth reading. The first pass narrows a fuzzy request down to plausible candidates from what’s actually on the server; a second pass takes those candidates and writes the actual explanation of why each one fits, grounded in what it was told is available rather than what it might assume exists. Separating “find the candidates” from “explain the candidates” turned out to matter more than any prompt wording — a single pass trying to do both tends to invent details about titles it was never actually shown.

Problems & solutions

Symptom: people type episode requests in every format imaginable, and my first attempt understood exactly one of them. Diagnosis: I’d guessed at how people type instead of watching how they actually do it. Fix: built up a set of patterns from real messages as they came in, rather than trying to predict them all up front like an idiot.

Symptom: “which one did you mean, 1, 2 or 3?” has nowhere to live in a chat where every message arrives with no memory of the last one. Diagnosis: there’s no such thing as a session in a text conversation. Fix: a short-lived memory of “what was I just asked” that expires on its own and survives a restart, so the bot doesn’t forget mid-conversation like it’s had a few pints.

Symptom: the first version of the “someone might like this” notification was generic enough that nobody read it. Diagnosis: I’d asked the model to be interesting without giving it anything interesting to work with. Fix: feed it what that specific person actually watches and ask for a reason tied to it. Suddenly people were replying to messages from a bot, which felt like a small victory.

Symptom: recommendations occasionally suggested things that were already sitting right there in the library. Diagnosis: the model had no idea what we already had. Fix: tell it. Obvious in hindsight, embarrassing that it wasn’t obvious at the time.

The stack

ComponentWhy
PythonOne maintainer, one machine — a script beats a framework every time here
Telegram Bot APIEveryone already has Telegram installed; nobody has to download anything new
Claude APIDoes the judging — what to recommend, what matches a vague description, who to bother with a notification

Results

The late-night messages have stopped, which was the entire point. The weekly cleanup turned an “I’ll deal with that library bloat someday” problem into a small, boring, regular decision, and it’s made a real dent in the backlog. The recommendation feature gets used far more than I expected, mostly because the reasons are specific — “you’ve been on a Richard Diamond kick, try this” lands better than any generic list ever did. The personalised notifications were the biggest surprise: a one-line reason tied to what someone actually watches gets a reply; a broadcast to everyone gets ignored by everyone.

What’s next

I want proper logging on every model call — right now only outright failures get logged, so a subtly wrong answer is invisible until someone notices the recommendation was rubbish. And the “who’d actually care about this” logic used for notifications deserves a second pass in the recommendation flow itself, so it can drip-feed suggestions over time instead of dumping five at once.