Franchisarr looks at your Plex, Jellyfin or Emby library and finds two things you probably want and don't have:
- Films missing from collections you already own part of. You have Beverly Hills Cop and II but not III — one click sends it to Radarr.
- Spin-offs of shows you already watch. You have NCIS but not NCIS: Los Angeles — one click sends it to Sonarr. Found through Wikidata, so it also knows Family Guy → American Dad!, which no name search could — and crosses between film and TV: own the film Serenity and it suggests the series Firefly.
And, built on the same data: franchise pages that put films and TV together (Star Wars — you have 8 of 15), director pages (you own 11 Nolan films; missing Following and Insomnia), an Upcoming page of announced films in franchises you own with release-date notifications, and import lists Radarr and Sonarr can poll so you never have to click Add.
It reads all three — one of them or several at once, with a film on any of them counting as owned and a tick on what you've watched — supports multiple Radarr and Sonarr instances, signs you in with your media-server account, scans on a schedule, tells you when it finds something new — Discord, Slack, or anything Apprise reaches (Telegram, Pushover, ntfy, email, a hundred more) — wears your theme.park theme like the rest of your stack, and has a web UI and a CLI. Everything it hides — low-rated films, TV specials, shorts — is a preference, folded away rather than deleted.
Franchisarr owes its starting idea to Gaps, which has found missing collection films in Plex since 2019. This is that idea extended to Jellyfin and Emby, to TV spin-offs and cross-media franchises, and to import lists the *arrs poll themselves.
Status: pre-1.0 and in daily use against a real library of ~3,400 films and ~660 shows. Images are published to GHCR for amd64 and arm64.
How it was built: largely written by Claude Code, Anthropic's AI coding tool — directed, tested against a real library and reviewed by a human. The core — matching, gaps, spin-offs, franchises, directors — was measured against that library as it was built (the numbers are in the changelog); what hasn't been tested for real is listed under What's been tested. There are over 900 tests with no live network calls, and the full git history was scanned for secrets before the repo went public. The code is MIT; read it.
| Collection detail | Franchise |
|---|---|
![]() |
![]() |
| TV spin-offs | Directors |
|---|---|
![]() |
![]() |
mkdir franchisarr && cd franchisarr
curl -fsSLO https://raw.githubusercontent.com/prophetizer/franchisarr/master/docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/prophetizer/franchisarr/master/.env.example -o .env
# edit .env — at minimum: your media server (PLEX_URL+PLEX_TOKEN, or JELLYFIN_URL+JELLYFIN_API_KEY,
# or EMBY_URL+EMBY_API_KEY), TMDB_API_KEY, ADMIN_USERNAME, ADMIN_PASSWORD
docker compose up -dThe image is ghcr.io/prophetizer/franchisarr (latest, or a version like 0.26.0). To build
from source instead, clone the repository and change image: to build: . in the compose file.
Then open http://localhost:8000, sign in, choose which libraries to scan, and run a scan.
You need a TMDb API key — the free v3 one from themoviedb.org/settings/api. Note it's the shorter value on that page; the v4 Read Access Token won't work, and Franchisarr will tell you so if you paste it by mistake.
Coming, after more testing. A Community Applications template is drafted in
contrib/unraid/, but it hasn't been run on Unraid yet, so it isn't
recommended. Until then it runs on Unraid like any other container: image
ghcr.io/prophetizer/franchisarr, port 8000, a path mapped to /config, and the environment
variables from the Configuration table below.
Almost everything can be set in the app itself — media servers under Servers, Radarr, Sonarr and Seerr under Instances, the rest under Settings. Environment variables are a convenience for docker-compose, and they behave in one of two ways:
- Seeded once: media servers, Radarr, Sonarr, the admin account, and the values Settings owns (TMDb and fanart.tv keys, schedule, notifications). Read on first boot only; after that the database wins, so change them in the app.
- Read every start:
BASE_URL,TZ,PUID/PGID,LOG_LEVEL,SESSION_COOKIE_SECURE,TRUSTED_PROXY_HOPS,SHOW_ARTWORK, theTP_*theme variables andUPDATE_CHECK. Change these and restart.
See .env.example for the full list.
| Variable | Purpose |
|---|---|
PLEX_URL, PLEX_TOKEN |
Your Plex server |
JELLYFIN_URL, JELLYFIN_API_KEY |
Your Jellyfin server (Dashboard → API Keys) |
EMBY_URL, EMBY_API_KEY |
Your Emby server |
MEDIA_SERVER |
Optional: seed only plex, jellyfin or emby when several pairs are set |
TMDB_API_KEY |
Your own free v3 key |
FANART_API_KEY |
Optional; adds franchise logos to collection headings |
SHOW_ARTWORK |
false turns off all poster and logo images |
RADARR_URL, RADARR_API_KEY |
Optional first Radarr; more can be added in the UI |
SONARR_URL, SONARR_API_KEY |
Optional first Sonarr |
ADMIN_USERNAME, ADMIN_PASSWORD |
Fallback login, created on first boot only |
BASE_URL |
e.g. /franchisarr when behind a reverse proxy subpath |
SCAN_SCHEDULE_CRON |
e.g. 0 3 * * *; empty disables scheduled scans |
TZ |
Which timezone the schedule runs in |
PUID, PGID |
Ownership of the config volume, as in the linuxserver.io images |
TRUSTED_PROXY_HOPS |
Reverse proxies in front of the app (default 1; 2 behind Cloudflare's proxy plus Traefik). Lets the sign-in rate limit see visitors' real addresses |
Spin-offs of shows you own are discovered from Wikidata during a scan — no key or account needed — and matched on TMDb ids rather than titles, so it finds the ones whose names give nothing away (Family Guy → American Dad!). Weaker evidence is marked possible, and nothing is ever added without you clicking.
Rather than clicking Add per film, let the *arrs pull from Franchisarr. Under Settings → Import lists, generate a key (Settings is for administrators); then in Radarr, Settings → Import Lists → Add → Custom List and paste a films URL, for example:
https://franchisarr.example.com/api/lists/films.json?api_key=YOUR_KEY&min_rating=7
Sonarr takes the shows URL the same way. The key is shown once, stored only as a hash, and can be
revoked from the same place. Radarr's and Sonarr's own settings then decide what
to monitor and where — Franchisarr never adds anything itself. Your dismissals and preferences apply to the
lists; min_rating on the URL overrides the household floor for that list only.
Collection screens show posters from TMDb. Adding a free fanart.tv key additionally puts the franchise wordmark over a backdrop at the top of each collection — the only thing that key is used for.
Both are fetched by the browser from image.tmdb.org and assets.fanart.tv, so a client with no
internet access, or anyone who would rather nothing left their network, can set
SHOW_ARTWORK=false for a text-only interface.
Use their address on your own network, not a public one:
services:
franchisarr:
networks: [arrs] # the network Radarr and Sonarr are already on
networks:
arrs:
external: trueRADARR_URL=http://radarr:7878
SONARR_URL=http://sonarr:8989
A public URL behind a login proxy — Authelia, Authentik, Cloudflare Access — will not work:
those answer API requests with a login page, and an API key can't get past one. Franchisarr says
so rather than blaming your API key. If you'd rather keep the public URL, add a bypass rule in
the proxy for /api so API-key requests are let through.
If your household routes requests through Seerr — or the Overseerr or Jellyseerr it grew out of — add it on the Instances page, with the API key from Seerr's own Settings → General. Every Add dialog then offers Request via Seerr next to the direct Radarr/Sonarr choice: Seerr picks the *arr, profile and folder from its own settings, and the request shows up in Seerr's history like any other. Open requests keep a title out of the lists the way a queued Radarr add does; declined and failed ones come back, since those are worth asking again.
One thing to know: Seerr's API key acts as its administrator, and administrators' requests are approved automatically. So a request made from Franchisarr normally goes straight through rather than waiting for someone to approve it — the result in the dialog says which happened.
In February 2026 the Overseerr and Jellyseerr teams
merged into Seerr — one codebase with both
projects' features — and Overseerr's own repository was archived. All three speak the same
/api/v1, so an older instance keeps working: pick which one you run when you add it, and only
the label changes.
The Upcoming page is also an iCalendar feed, so announced films in franchises you own appear in your calendar app — one all-day event per film, with the collection and how much of it you have:
https://franchisarr.example.com/api/lists/upcoming.ics?api_key=YOUR_KEY
Apple Calendar: File → New Calendar Subscription. Google Calendar: Other calendars → From URL. Same key as the import lists (Settings → Import lists), and the URL is shown there. It refreshes daily; films with no date yet appear once TMDb gives them one.
After a scan that finds something new — scheduled or started by hand, never every run, and
never the very first scan, whose findings are the state of your library rather than news —
Franchisarr sends one message. Settings → Notifications takes a Discord or Slack webhook, a generic JSON webhook for
your own automation, or Apprise URLs, one per line (tgram://…, pover://…, ntfy://…,
mailto://…; the Apprise wiki lists every service),
delivered from inside the app. If you already run
apprise-api, point it at that instead and keep the
destinations there. The URL is treated as a credential: redacted in logs and in the redacted
config export.
/api/lists/stats.json?api_key=… returns the headline numbers as one flat document — collections
with gaps, missing films, spin-offs, upcoming films, incomplete franchises, directors, library
size, last scan — cached for a minute. For Homepage:
- Franchisarr:
icon: https://raw.githubusercontent.com/prophetizer/franchisarr/master/docs/brand/icon-512.png
href: https://franchisarr.example.com
widget:
type: customapi
url: https://franchisarr.example.com/api/lists/stats.json
headers:
X-Api-Key: your-key
refreshInterval: 300000
mappings:
- field: missing_films
label: Missing films
format: number
- field: collections_with_gaps
label: Collections
format: number
- field: missing_spinoffs
label: Spin-offs
format: number
- field: upcoming_films
label: Upcoming
format: numberThe key is the one from Settings → Import lists; the header keeps it out of your dashboard's
URL list, but ?api_key= works too for widgets that can't send headers.
Sign in with your media server account. For Plex that is the usual OAuth button, and Franchisarr never sees your Plex password; only accounts that can actually reach your Plex server are admitted, so having a Plex account isn't enough. For Jellyfin and Emby it is a username and password, checked against the server itself. Who administers Franchisarr follows from the server: with Plex it's the server's owner, with Jellyfin and Emby it's anyone who is an administrator there — checked again every time they sign in, so handing the server over, or demoting someone on Jellyfin, takes their admin rights here with it.
By default only administrators can sign in. Being able to reach your Plex server is a much wider group than the people who run your house, so friends and family you share it with are refused. If you want them in, turn on Settings → Who can sign in. They can then browse the lists and hide titles for themselves, but they can't add anything to Radarr, Sonarr or Seerr, start scans, or change settings, and they don't see those controls. Turning it off again signs them out and stops their API keys. Settings → Users lists everyone who has signed in, and can sign someone out everywhere, revoke their API key or remove them.
The local admin account from ADMIN_USERNAME/ADMIN_PASSWORD is the fallback for when no server
is configured yet, or plex.tv is unreachable. It's created on first boot only, so changing those
variables later has no effect — if you forget the password:
docker exec -it franchisarr python scripts/reset_admin_password.py # lists accounts
docker exec -it franchisarr python scripts/reset_admin_password.py admin # prompts for a new oneIf no sign-in button is offered, Franchisarr couldn't reach your server at startup — with Plex it has to know which server it belongs to before it can check that an account is allowed in. Press Test on the server under Servers, or check that server's address and key and restart.
On first boot every filled-in pair in .env becomes a server; after that, add and change them
under Servers in the app (name, address, key; a watched as username for Jellyfin/Emby, since an API key belongs to
nobody). Their libraries are chosen together on the Libraries page and scanned in one pass. A
film on two servers is owned once; its tile on a collection page says which servers hold it. Watched films get a
tick, and the Collections page can be filtered to franchises you've actually started.
The CLI talks to Franchisarr's own API, so it works through docker exec or from anywhere that
can reach the app.
export FRANCHISARR_URL=http://localhost:8000
export FRANCHISARR_API_KEY=... # an admin's key: Settings, or `cli.py api-key`
python cli.py scan
python cli.py gaps
python cli.py add 176 --instance 1scan walks your libraries, films and TV together; gaps and spinoffs list what's missing;
review shows matches that need confirming; add, add-show and add-collection send things to
Radarr/Sonarr; instances and activity inspect the rest. Scanning and adding need an
administrator's key, like the buttons they stand in for.
Nord (dark) by default, with a light toggle to Nord's light half. theme.park themes are set by environment variable, using theme.park's own names — so if your stack already sets these, Franchisarr picks the theme up with no per-app configuration:
environment:
TP_THEME: nord
TP_DOMAIN: theme-park.dev # or your own self-hosted copy
TP_SCHEME: https
TP_COMMUNITY_THEME: "false"THEME_CSS_URL overrides those with a literal stylesheet URL.
If you theme centrally by injecting a stylesheet at the proxy — nginx sub_filter, a Traefik
plugin, theme.park's Docker mod — that works with nothing set here at all. Franchisarr always
loads a small adapter mapping theme.park's custom properties onto the ones it paints with, so an
injected theme-options stylesheet takes effect on its own. The whole page follows the theme,
header icon included — verified against all eleven official theme options
(screenshots).
Franchisarr's base stylesheet — the one a proxy or the theme.park mod injects when you point
it at app=franchisarr — was
merged into theme.park on 2026-09-22.
It is on their develop branch, so develop.theme-park.dev serves it today and
theme-park.dev will once they cut a release. If you host your own copy,
contrib/theme-park/franchisarr-base.css is the same file, ready to drop in as
css/base/franchisarr/franchisarr-base.css (see
contrib/theme-park/README.md).
Leave it all unset and no stylesheet is fetched from anywhere but your own server.
Honest about what has run against the real thing and what has only run against the test suite:
| Piece | Tested against |
|---|---|
| Plex | The developer's own library — ~3,400 films, ~660 shows — every day |
| Jellyfin 10.11, Emby 4.9 | Real servers during development, library scans and sign-in both; not in daily use |
| Seerr 3.4.1 | End to end on a real instance: connection, request cache, and a real request through to Radarr (September 2026). That first live test found three bugs, fixed in 0.23.1 |
| Radarr 6.3, Sonarr 4.0 | Real instances, one of each, every day |
| Several Radarr or Sonarr instances | The test suite only — the developer runs one of each |
| Overseerr, Jellyseerr | The test suite only; they speak Seerr's API, which is tested for real |
| Unraid template | Not yet run on Unraid — coming after more testing |
| Large libraries | A synthetic 20,000-film, 2,000-show library (scripts/loadtest.py) |
| Sign-in for people you share with | The test suite only: refused by default, the Settings switch, and what they can and can't do once let in. No real shared account has signed in on the developer's install |
If your setup is in one of the lower rows and something doesn't work, that's the most useful bug report there is.
Everything Franchisarr contacts, from the server:
| Service | When | Why |
|---|---|---|
TMDb (api.themoviedb.org) |
During scans | Collections, films, shows, directors — needs your free key |
Wikidata (query.wikidata.org) |
During scans | Spin-offs, continuations and franchises. No account; requests carry a User-Agent naming this project, as Wikidata asks |
| plex.tv | Only when someone signs in with Plex | The sign-in PIN flow, and checking the account can reach your server |
| fanart.tv | Only if you set a fanart.tv key | Franchise logos |
GitHub (api.github.com) |
When Settings is opened | The update banner. Sends nothing about you or your library. Off under Settings → Update check, or with UPDATE_CHECK=false |
Plus whatever you point it at yourself: your media servers, Radarr, Sonarr, Seerr, a webhook or Apprise, a theme.park host. In the browser, posters load from TMDb's image CDN and logos from fanart.tv's.
No telemetry, no analytics, no accounts with anyone but the services above.
Settings has a config download. The full one contains your media server credentials and every API key in plain text — treat it like a password. There's a redacted download alongside it with those blanked out; that's the one to paste into a forum thread when asking for help.
Importing a backup restores settings, instances, spin-off mappings and your hidden titles. Media servers in it arrive switched off: a server decides who can sign in, so check each one on the Servers page and switch it on. Anything in the file that isn't a known setting or field is skipped.
docker compose pull franchisarr && docker compose up -d franchisarrThe database upgrades itself on start, and your settings, instances and dismissals are kept. The Settings page says when a new version is out (unless you've turned the update check off). Before a jump, skim the changelog: anything that changes how an existing install behaves has an Upgrading note — 0.25.0, for example, signs out everyone who isn't an administrator until you choose to let them in.
Settings also has Download diagnostics: version, library and cache counts, the titles that didn't match or need review, and the last scan's result, with every credential blanked. Attach it to a bug report — it answers most of what would otherwise be the first round of questions. Security issues go through private reporting, not a public issue.
Franchisarr can only work with an item if it can resolve it to a TMDb ID. Settings → Download diagnostics lists what didn't match on any server, which is the quickest way to see the shape of the problem. For Plex specifically there is also a standalone audit:
export PLEX_URL=http://your-plex-host:32400
export PLEX_TOKEN=your-plex-token
python scripts/plex_guid_audit.pyIt's read-only, and Plex-only — Jellyfin and Emby report their provider IDs directly, so there is nothing equivalent to audit. For each library it reports how many items carry a TMDb ID, which GUID formats are in use, and a sample of what didn't resolve. Libraries of home videos, concert rips or test clips will legitimately show 0% — untick those in Franchisarr rather than trying to match them.
If it ends with an UNRECOGNISED AGENTS section, please report it: your library uses a GUID format
Franchisarr doesn't parse yet, and that section says exactly what's needed.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
pytest
DB_PATH=./franchisarr.db ADMIN_USERNAME=admin ADMIN_PASSWORD=change-me uvicorn app.main:app --reloadTests never touch the network — Plex, Jellyfin, Emby, TMDb, Wikidata, fanart.tv, Radarr, Sonarr and Seerr are all mocked. See docs/DEVELOPMENT.md for
the conventions this codebase holds itself to, and docs/DESIGN.md for the design and the
28 documented technical challenges behind it.
See SECURITY.md for how to report vulnerabilities.








