# Tom Gould

> Development Lead at Bright Sites. I build publishing software, small apps and open-source libraries.

Writing about how I build things, mostly the products here, and a page for each product: what it does, how it's built and where its code is.

Each link below is the Markdown version of a page. Every page listed here has one at its own address with .md added (the front page’s is https://www.dealmobile.co.uk/index.md), and a request that asks for text/markdown gets it at the page’s usual address.

## Writing

- [A Markdown version of every page, for AI agents](https://www.dealmobile.co.uk/writing/a-markdown-version-of-every-page.md): Why every page on this site now has a Markdown version for coding agents, the three ways an agent finds it, what goes into it, and how it stays out of search results and caches.
- [A TV remote from the command line](https://www.dealmobile.co.uk/writing/a-tv-remote-from-the-command-line.md): Pairing a Linux computer with a Google TV over the protocol phone remotes use, what the six-character code on the screen really checks, and why a remote that can’t change anything is worth having next to ADB.
- [The packages I kept on a Google TV](https://www.dealmobile.co.uk/writing/the-packages-i-kept-on-a-google-tv.md): The other half of decluttering a TCL Google TV: a keep-list of 35 packages that debloat lists remove or that only look disposable, how each one got there, and the script that refuses to touch them.
- [Letting an agent drive the TV](https://www.dealmobile.co.uk/writing/letting-an-agent-drive-the-tv.md): The rules a coding agent worked under while it changed a TV we were still watching: only changes that can be undone, small batches with a person testing in between, a log of everything, and the mistakes that added the rest.
- [A fourth screen on the living-room TV](https://www.dealmobile.co.uk/writing/a-fourth-screen-on-the-living-room-tv.md): Giving a Linux laptop an extra 4K screen that only exists while the TV is watching, with Sunshine and Moonlight to stream it, a virtual display driver instead of a dummy plug, and the black screens and lost layouts on the way.
- [Decluttering a TCL Google TV without rooting it](https://www.dealmobile.co.uk/writing/decluttering-a-tcl-google-tv.md): Switching off 34 packages on a TCL Google TV over ADB, replacing the home screen, and finding the TCL-only permission that stopped the replacement surviving a restart.
- [Turning saved web pages into clean Markdown](https://www.dealmobile.co.uk/writing/turning-web-pages-into-clean-markdown.md): How ScrapePy finds the article inside a saved page, strips the menus, adverts and sign-up boxes around it, and points its links at the other Markdown files.
- [Testing an API client without the network](https://www.dealmobile.co.uk/writing/testing-an-api-client-without-the-network.md): How the Puzzler PHP SDK signs its requests and turns HTTP errors into exceptions, and how one protected method lets the tests check both with no credentials and no connection.
- [Taxing a household one earner at a time](https://www.dealmobile.co.uk/writing/taxing-a-household-one-earner-at-a-time.md): How Where Should I Live taxes each earner alone, lays a country’s joint and family rules on top, and proves that one person’s figures haven’t moved at all.
- [Rewriting an old PHP scraper around curl_multi](https://www.dealmobile.co.uk/writing/rewriting-an-old-scraper-around-curl-multi.md): How Scrape’s download loop went from one file at a time to a pool of parallel cURL transfers, and what the rewrite lost from the older code that read directory listings.
- [Picking the cheapest model that can do the job](https://www.dealmobile.co.uk/writing/picking-the-cheapest-model-that-can-do-the-job.md): How Mullet sizes up a task, picks the cheapest model that clears a quality floor for it, and carries on when OpenRouter withdraws a model without notice.
- [One JSON shape for RSS, Atom and RDF feeds](https://www.dealmobile.co.uk/writing/one-json-shape-for-rss-atom-and-rdf.md): How RSS to JSON turns three feed formats into one predictable array, and the namespace cases that took two attempts to get right.
- [Live coverage on Apple News from PHP](https://www.dealmobile.co.uk/writing/live-coverage-on-apple-news-from-php.md): How the Apple News API library builds Live Coverage articles, the rules it checks before Apple does, and the request metadata that starts and ends the coverage.
- [Indexing code by function, class and method](https://www.dealmobile.co.uk/writing/indexing-code-by-function-class-and-method.md): How Trove splits source code into functions, classes and methods with tree-sitter, re-indexes only the files that changed, and where its extractors fall short.
- [Finding the dull parts of a screen recording](https://www.dealmobile.co.uk/writing/finding-the-dull-parts-of-a-screen-recording.md): How Reelify scores each frame of a screen recording by how much it changed, turns the scores into cuts with a margin around them, works out a speed to fit a target length and cuts out each segment, checked against a test clip with known content.
- [Cloning every repository without a token in the URL](https://www.dealmobile.co.uk/writing/cloning-repositories-without-a-token-in-the-url.md): How GitHub Auditor keeps a clone of every repository up to date, gives git its token through a short-lived GIT_ASKPASS script instead of the URL, and reports force pushes, dirty clones and deleted repositories.
- [Writing down an upload protocol and testing servers against it](https://www.dealmobile.co.uk/writing/testing-servers-against-an-upload-protocol.md): How TurboPush’s chunked upload protocol is written down in PROTOCOL.md, how a small Node suite checks any server against it over HTTP, and the two bugs it found in my own PHP endpoint.
- [Fetching a feed from a URL you don’t trust](https://www.dealmobile.co.uk/writing/fetching-feeds-from-untrusted-urls.md): How RSS to JSON stops a feed URL from reaching private addresses on the server’s own network, by checking every address a host resolves to, pinning the connection to the one it checked, and checking every redirect again.
- [A live calendar feed and badge from a Cloudflare Worker](https://www.dealmobile.co.uk/writing/calendar-feed-and-badge-from-a-worker.md): How the Solar Calendar serves a feed people can subscribe to in their calendar app, and a badge with today’s date, from a small Cloudflare Worker that runs the same date code as the app.
- [Counting developer activity from git](https://www.dealmobile.co.uk/writing/counting-developer-activity-from-git.md): How GitHub Auditor counts commits, lines and reviews across many repositories without counting the same work twice, counting lines nobody wrote, or passing off missing data as a quiet week.
- [Combining keyword and vector search in Trove](https://www.dealmobile.co.uk/writing/combining-keyword-and-vector-search.md): How Trove runs BM25 and vector search side by side, puts their scores on one scale, and lets a cross-encoder decide the final order, with an example of the reranker overruling both.
- [Checking a delegate’s edits before trusting them](https://www.dealmobile.co.uk/writing/checking-a-delegates-edits.md): How Mullet decides what a cheaper model actually changed, and flags the edits worth a second look, without asking another model and without trusting the first one’s word.
- [Saving websites for offline reading with ScrapePy](https://www.dealmobile.co.uk/writing/saving-websites-for-offline-reading.md): What it takes to turn a live website into a copy that works with no connection, and how ScrapePy maps URLs to files, rewrites links and only re-downloads what has changed.
- [One source of truth behind a static app](https://www.dealmobile.co.uk/writing/one-source-of-truth-behind-a-static-app.md): How the second version of Where Should I Live keeps its data in a headless CMS, ships it as static JSON, and refuses to build if the data has drifted.
- [Making large uploads resumable with TurboPush](https://www.dealmobile.co.uk/writing/making-large-uploads-resumable.md): How TurboPush splits big files into chunks, retries the ones that fail, carries on after a page reload, and keeps the PHP side safe when chunks arrive twice or all at once.
- [Why the Solar Calendar has 13 months of 28 days](https://www.dealmobile.co.uk/writing/why-the-solar-calendar-has-13-months.md): The rules behind the Solar Calendar, why the year starts on the first Monday after the spring equinox, and why the rebuild swapped special days for a leap week.
- [Publishing to Apple News from PHP](https://www.dealmobile.co.uk/writing/publishing-to-apple-news-from-php.md): What it takes to publish an article to Apple News from PHP, from signing requests and building multipart uploads to the JSON details Apple’s format is strict about.

## Products

- [Where Should I Live](https://www.dealmobile.co.uk/products/where-should-i-live.md): A city-ranking app that works out your take-home pay, monthly costs and visa options in 78 cities, for one person or a whole household. A headless CMS holds the data, and a static app ships it.
- [Solar Calendar](https://www.dealmobile.co.uk/products/solar-calendar.md): A calendar where every month is the same four weeks, the year keeps pace with the seasons, and ordinary dates are shown alongside every day.
- [Apple News API](https://www.dealmobile.co.uk/products/apple-news-api.md): A PHP 8.1 library for the Apple News Publisher API. It signs requests, builds Apple News Format documents in PHP objects and handles articles, sections and channels.
- [TurboPush](https://www.dealmobile.co.uk/products/turbopush.md): A dependency-free TypeScript upload client and a single-class PHP endpoint. Files go up in parallel chunks with retries, pause and resume, and the client also speaks the open tus protocol.
- [RSS to JSON](https://www.dealmobile.co.uk/products/rss-to-json.md): Parses RSS 0.9x and 2.0, RSS 1.0, Atom and other XML feeds into plain PHP arrays ready for json_encode, keeping namespaced elements such as media:content and dc:creator.
- [Puzzler PHP SDK](https://www.dealmobile.co.uk/products/puzzler-php-sdk.md): A small PHP library for publishers who license puzzles from Puzzler Media. It signs each request, filters puzzles by date, type and name, and maps HTTP errors to exceptions.
- [Mullet](https://www.dealmobile.co.uk/products/mullet.md): A Python MCP server and command-line tool that scores each coding task, routes it to a local or cheap cloud model within a budget, checks the files a delegate changes, and searches your own documents and code.
- [GitHub Auditor](https://www.dealmobile.co.uk/products/github-auditor.md): A Python CLI that keeps a local clone of every repository in your GitHub and Bitbucket accounts and writes Markdown reports from them. It can also archive, license, tag and reconfigure repositories in bulk.
- [ScrapePy](https://www.dealmobile.co.uk/products/scrapepy.md): A command-line crawler that downloads a site with its images, rewrites the links so it works offline, and writes a Markdown copy with frontmatter. Re-runs only fetch what has changed.
- [Trove](https://www.dealmobile.co.uk/products/trove.md): A Python search engine for your own documents and code. It runs keyword and vector search together, re-ranks the results with a cross-encoder, and answers AI coding tools through an MCP server with 38 tools.
- [Scrape](https://www.dealmobile.co.uk/products/scrape.md): A single PHP class that walks the “Index of /” pages a web server generates, filters the files by type, path and name, and downloads them in parallel.
- [TCL Google TV Tools](https://www.dealmobile.co.uk/products/tcl-google-tv-tools.md): Shell and Python scripts that switch off a TCL Google TV’s unwanted packages in small, reversible batches, get round TCL’s block on apps starting at boot, check the setup after every restart, and give a Linux laptop a 4K screen on the TV that only exists while the TV is watching.
- [Reelify](https://www.dealmobile.co.uk/products/reelify.md): A local Python pipeline that scores the motion in each frame of a screen recording, cuts the idle parts, speeds up the rest to fit a target length and can caption keyframes with a local vision model.

## Pages

- [About](https://www.dealmobile.co.uk/about.md): Who I am, what I work on, how I work, and the things I build in my own time.
- [Accessibility](https://www.dealmobile.co.uk/accessibility.md): How this site aims to be accessible, and how to report a problem.
- [Cookies](https://www.dealmobile.co.uk/cookies.md): The one cookie this site uses, and what it keeps in your browser instead.
- [CV](https://www.dealmobile.co.uk/cv.md): Work history since 2004, skills, the products on this site and a recommendation.
- [How this site is built](https://www.dealmobile.co.uk/colophon.md): A theme written for this site, almost no third-party requests, and the solar year under the name.
- [Legal notice](https://www.dealmobile.co.uk/legal.md): Who runs this site and where it’s hosted.
- [Privacy notice](https://www.dealmobile.co.uk/privacy.md): What personal data this site collects, why, how long it’s kept and your rights.
- [Terms of use](https://www.dealmobile.co.uk/terms.md): The terms for using this site and its content.
- [Why dealmobile.co.uk?](https://www.dealmobile.co.uk/dealmobile.md): This site lives on the domain of Deal Mobile, the phone deals site I built, which ran from 2011 to 2019.
