Share any link from your iPhone (a reel, a TikTok, a YouTube video, an article, a film's page on Letterboxd, AlloCiné or Wikipedia): Spotarr finds the movie or TV show it is about, and adds it to your library through Seerr. A personal project, for one user, self-hosted with Docker or on Umbrel.
- 🎞️ Watches and listens. A video (Instagram, TikTok, YouTube…) is downloaded, then an LLM looks at a few frames and hears the sound. The title is often only said out loud.
- 📰 Reads any page. A page linking to IMDb or TMDB (Letterboxd, Wikipedia) names its work at once, without the LLM. Any other link (an article, AlloCiné, a photo post, a poster) is read by the LLM: the page's title, description, text and pictures.
- 🎯 Confirms on TMDB. It keeps the titles that exist on TMDB, with a poster and a confidence. A video or an article naming three films gives three candidates.
- 📚 Knows your library. Seerr tells what is already there.
- ➕ Adds in one click, or on its own: auto add requests a candidate on its own when one of your rules holds (movies or TV shows, a single candidate, confidence, TMDB rating), with a preview of what each rule would have added. A TV show gets season 1 only.
- 📺 Says where to watch it already: pick your country and the streaming platforms you subscribe to (Settings → Streaming), and a candidate streaming on one of them links to it, next to a link to your library (Jellyfin, Plex) when the title is there. No need to add what you can already watch.
- ⭐ Keeps the rest for later. A rule can put candidates in Seerr's watchlist instead of requesting them: nothing is downloaded, JellyTV shows them, and a title leaves the watchlist once available. Seerr must use Jellyfin or Emby: it hides its own watchlist from Plex accounts.
- 📺 Waits for you on the TV. With Jellyfin, a collection keeps what you spotted, once downloaded and until watched: under Collections, on the Apple TV too.
- 🖥️ A web UI for the history, the candidates and the settings, in light and dark.
On Umbrel: add Mathieu's community app store and install Spotarr. Sign in with the default password umbrelOS shows: it is the API token.
With Docker Compose:
services:
spotarr:
image: ghcr.io/mathieuletyrant/spotarr:latest
ports:
- "3000:3000"
volumes:
- ./data:/data
restart: unless-stoppedRun docker compose up -d, then open http://localhost:3000. The API token is printed in the logs at every start (docker compose logs spotarr), and the UI asks for it once. Optional environment variables: PORT (default 3000), DATA_DIR (default /data), and API_TOKEN, which sets the token instead of generating one.
Then, in Settings:
- Paste an OpenRouter API key (or any OpenAI-compatible API) and a TMDB read access token.
- Set Seerr's URL and API key.
- Optional: in the Jellyfin tab, set Jellyfin's URL, an API key (Dashboard → API Keys) and your user. Spotarr keeps a collection, "Spotted", of the titles added from a Share that Jellyfin has and you have not watched. It shows under Collections, in the browser and in JellyTV.
- Check that the Status at the top shows everything as Working.
An identification costs about $0.01 with the default model. Settings shows the cost of the last 30 days.
shortcuts/Spotarr.shortcut puts Spotarr in the share sheet for links: share one, and a notification says "Sent to Spotarr", or shows Spotarr's error.
- Open the file on the iPhone (AirDrop it, or open it from GitHub), or on a Mac signed in to the same iCloud account.
- Add it, then edit it (Shortcuts → Spotarr → ⋯): in the second Text action, replace
PASTE-YOUR-API-TOKEN-HEREwith the API token (Settings → Access → Copy). The first one is Spotarr's address,http://umbrel.local:3838on Umbrel. - In any app, Share → More → Spotarr. The link shows up in the History.
From outside home, through Cloudflare Access (Spotarr itself needs nothing more):
- In the Cloudflare tunnel, add a public hostname, e.g.
spotarr.example.com, pointing to the Umbrel's address on port3838. - In Zero Trust → Access, add an application for that hostname, with two policies. The first is Service Auth with a new service token: copy its Client ID and Client Secret, since the secret is shown once. The second is Allow, with your email, for the web UI in a browser.
- In the Shortcut, set the first Text action to
https://spotarr.example.com, and paste the Client ID and Client Secret into the third and fourth. Then it works at home and away. Left empty, the headers are sent empty, which is fine at home.
If Cloudflare refuses the call, the notification shows its answer instead of "Sent to Spotarr".
The file holds no secret, so it can be shared; once imported, it keeps your token in plain text, so do not share that copy. It is generated by bun shortcuts/build.ts (macOS only, signs it with shortcuts sign).
Build it by hand instead
New Shortcut, ⓘ → Show in Share Sheet, receiving URLs. Then these actions:
- Get Contents of URL:
http://umbrel.local:3838/api/spotarr.v1.SpotarrService/CreateShare, method POST.- Headers:
Authorization=Bearer <your API token>. Through Cloudflare, alsoCF-Access-Client-IdandCF-Access-Client-Secret. - Request body: JSON, with one text field
url= Shortcut Input.
- Headers:
- Get Dictionary Value
sharein Contents of URL. - If Dictionary Value has any value: Show Notification "Sent to Spotarr".
- Otherwise: Show Notification "Not sent to Spotarr", with Contents of URL as its body.
Spotarr answers MCP (Model Context Protocol) clients at /mcp, with three read-only tools: list_shares, the History, filterable by genre, keyword, text and Summary, each Candidate with its TMDB genres, keywords, rating, overview and whether it was added; list_genres, the genres the filter accepts; and list_keywords, the TMDB keywords it accepts ("based on novel or book"). An assistant like Claude can then pick "a comedy for tonight" among what you shared.
Give your MCP client the MCP URL from Spotarr's Settings → Access (http://umbrel.local:3838/mcp?token=… on Umbrel). The token is in the URL because some MCP clients send no header of ours; it only reads, and the API refuses it. Regenerate it there if it leaks: the old URL stops working at once.
pnpm install --frozen-lockfile
pnpm dev:setup # once: fills ./data from .env and prints the API token
pnpm dev # the service on :3000, the web UI on http://localhost:5173
pnpm check # the whole gate: lint, contract, generated code, types, tests
Needs pnpm (its version comes from package.json), Bun (.bun-version), and brew install ffmpeg yt-dlp. Commands, conventions and known traps are in AGENTS.md, and the vocabulary is in GLOSSARY.md.
| Path | Role |
|---|---|
📜 packages/contract |
Protobuf contract of the API, generated TypeScript |
🎨 packages/design-tokens |
Design tokens, generated CSS variables |
⚙️ apps/server |
The service: Hono on Bun, Connect-RPC, SQLite |
🖥️ apps/web |
The web UI: React, TanStack Router and Query, Tailwind |
📱 shortcuts |
The iOS Shortcut and the script that builds it |
🧭 docs/adr |
Architecture decisions |
Three documented workarounds: Connect-RPC is mounted inside Hono through a small adapter (ADR 0001), Kysely talks to bun:sqlite through an in-house dialect (ADR 0002), and an Instagram photo post, which yt-dlp cannot read, is read from the Open Graph tags of its public page (apps/server/src/integrations/instagram/post.ts): its caption, its author and its first picture only.
Roadmap: milestones and the project board.
MIT.
