Finishing touches for Sonarr/Radarr libraries on Windows. After every import (or during a nightly sweep), each series or movie folder gets a poster-based folder icon, a rating suffix in the folder name, a Links/ subfolder of handy web shortcuts, and a hover tooltip with the plot summary.
git clone https://github.com/roko-tech/arr_finisher.git
cd arr_finisher
python -m pip install -r requirements.txt
copy .env.example .env
notepad .env :: fill in your API keys
python arr_finisher.py --validate
Then in Sonarr/Radarr: Settings → Connect → Custom Script, point at
<repo>\arr_finisher.bat, and trigger on On Import + On Upgrade.
| Output | How |
|---|---|
| Folder icon from the show's poster | Generates folder.ico natively (Pillow + Windows shell APIs) — a port of maforget/Folder-Icon-Creator, the original motivation for this project |
| Rating suffix in folder name | [IMDb 8.6], [MDL 7.5], or [MAL 9.3] — picks the best source automatically (see below) |
Links/ subfolder with shortcuts |
IMDb, Parents Guide, Trakt, Simkl, Letterboxd, MyDramaList, MyAnimeList, Twitter, combined Subtitle (SubDL + Subsource + OpenSubtitles) |
| Explorer tooltip | OMDb plot summary + rating shown on hover, via desktop.ini InfoTip |
| Sonarr/Radarr path sync | Folder rename is mirrored via API, with rollback on API failure. Every rollback is journaled to .rollbacks.log (OK when the disk was restored; FAIL for the rare double-failure where disk and service are left out of sync). Run --check-rollbacks to surface any FAIL entries |
- Korean content → MyDramaList (via the unofficial kuryana API)
- Anime → MyAnimeList. The anime is resolved to a MAL id deterministically by ID — Sonarr's TVDB id / Radarr's TMDB id → MAL id via the Fribb/anime-lists cross-map (cached locally) — then the live
meanis read from MAL's official API. Falls back to MAL's own title search when the ID isn't in the map, and to AniList's live score when MAL has none. - Everything else → IMDb. Primary: IMDb's public GraphQL endpoint (live ratings, no API key). Fallback: OMDb — used when GraphQL is unreachable, and for the plot-summary tooltip.
For non-anime, a title-similarity + year filter rejects bad fuzzy matches, so you don't accidentally end up with the wrong rating for "Bones (2005)" vs some other show. Anime skip fuzzy matching entirely when the TVDB/TMDB id is in the map.
Sonarr/Radarr fires On Import / On Upgrade
│ (sets env vars: Sonarr_Series_* / Radarr_Movie_*)
▼
arr_finisher.bat ─► python arr_finisher.py
│
├─ detect language / type (env vars first, service API as fallback)
├─ fetch rating from MAL / MDL / IMDb (in that priority order)
├─ rename folder, appending "[SOURCE X.X]"
├─ PUT new path back to Sonarr/Radarr (rollback on API rejection)
├─ generate folder.ico from the poster + bind it via the shell API
├─ create Links/ subfolder with .lnk + Subtitle.vbs shortcuts
└─ write plot summary into desktop.ini as Explorer InfoTip
Other Sonarr/Radarr events (Grab, Rename, Test, …) are acknowledged and
ignored — only Download events trigger the work above.
Prerequisites:
- Windows (
pywin32is used to create.lnkshortcut files; folder icons use Windows shell APIs) - Python 3.8+
Pillow(installed viarequirements.txt) — builds thefolder.ico; no external tool needed
Install the script:
git clone https://github.com/roko-tech/arr_finisher.git
cd arr_finisher
python -m pip install -r requirements.txt
copy .env.example .env
Then edit .env with your API keys (see Configure below) and verify:
python arr_finisher.py --validate
All config lives in .env (created from .env.example). Real OS environment
variables override the file.
| Key | What it's for |
|---|---|
OMDB_API_KEY |
Free key from omdbapi.com. Needed for the plot-summary tooltip, and as the IMDb-rating fallback when IMDb's GraphQL endpoint is unreachable |
SONARR_API_KEY |
Sonarr → Settings → General → API Key. Required if you'll trigger from Sonarr |
RADARR_API_KEY |
Radarr → Settings → General → API Key. Required if you'll trigger from Radarr |
| Key | Default | What it's for |
|---|---|---|
SONARR_API_URL |
http://localhost:8989 |
Override if Sonarr isn't on the local host |
RADARR_API_URL |
http://localhost:7878 |
Same, for Radarr |
SUBDL_API_KEY |
— | Resolves direct SubDL URLs for the Subtitle shortcut (without it, the shortcut falls back to a search URL) |
OPENSUBTITLES_API_KEY |
— | Same idea, for OpenSubtitles |
SEARCH_LANGUAGE |
ar |
ISO code (ar, en, ja, …) used by the Twitter hashtag filter and OpenSubtitles search URL |
KURYANA_BASE_URL |
https://kuryana.tbdh.app |
Self-host or alternate mirror |
MAL_CLIENT_ID |
— | Client ID from MAL → Preferences → API (public reads only, no OAuth). Fetches an anime's live mean score from MyAnimeList's official API. Strongly recommended; when unset, anime scoring degrades to the keyless AniList fallback (a different 0-10 scale) |
TRAKT_CLIENT_ID |
— | Public Trakt application client ID. Resolves TVDB/TMDB IDs to exact Trakt title pages; shortcuts fall back to title-and-year search when unset or unavailable |
SIMKL_CLIENT_ID |
— | Public Simkl application client ID. Resolves TVDB/TMDB IDs to exact Simkl title pages; shortcuts fall back to typed anime/TV/movie search when unset or unavailable |
FRIBB_ANIME_LIST_URL |
Fribb anime-list-full.json |
The TVDB/TMDB/IMDb→MAL cross-map, downloaded once and cached in .anime_map_cache.json. Override only to self-host/mirror |
ARR_FINISHER_ANIME_MAP_TTL_DAYS |
7 |
How long the cached anime ID map stays fresh before it's re-downloaded |
ANILIST_API_URL |
https://graphql.anilist.co |
Keyless anime-score fallback (queried by MAL id). Override only to self-host |
IMDB_GRAPHQL_URL |
https://caching.graphql.imdb.com/ |
Override if IMDb ever moves the endpoint |
ARR_FINISHER_LOG_LEVEL |
INFO |
Set to DEBUG for permanently-verbose logs |
ARR_FINISHER_LOG_DIR |
repo dir | Where to write arr_finisher.log (falls back to %TEMP%) |
ARR_FINISHER_RATING_CACHE_TTL_DAYS |
7 |
How long a rating stays fresh before sweep re-fetches it |
ARR_FINISHER_SWEEP_ROOTS |
— | Override sweep roots (otherwise auto-discovered from Sonarr/Radarr). Pipe-separated path:service pairs |
SUBDL_API_KEY and OPENSUBTITLES_API_KEY are listed as optional here, but
--validate will still report them as "missing" when unset. That's a heads-up,
not a failure — the rest of the script works without them.
In Sonarr/Radarr go to Settings → Connect → Custom Script and set the path
to <repo>\arr_finisher.bat. Trigger on On Import and On Upgrade (both
fire the same Download event). The .bat auto-locates the .py via %~dp0,
so you can keep the repo anywhere.
The Test event Sonarr/Radarr fires when you click "Test" in the UI is
acknowledged but does nothing. Other events (Grab, Rename, …) are
deliberately ignored.
First-run troubleshooting. The webhook silently swallows stdout/stderr (so
a crash never breaks the import flow). To make missing config visible, the
script writes an arr_finisher_setup.txt next to itself when it detects
missing keys. It's auto-deleted once config is healthy. Example contents:
arr_finisher needs configuration before it can do its job.
Last checked: 2026-05-16T11:40:15
Edit `.env` in the repo directory and set:
- OMDB_API_KEY
Free key from https://www.omdbapi.com/apikey.aspx — required for any rating fetch.
Then re-trigger the import in Sonarr/Radarr, or run:
python arr_finisher.py --validate
for a full health check.
The webhook handles every new import in full. The sweep is a rating-only follow-up that keeps ratings fresh as they evolve over time. Folder icons, shortcuts, and tooltips are not re-touched during sweep — the hook already did that when the content was imported.
What the sweep does, per folder:
- Looks up the IMDb ID via Sonarr/Radarr
- Skips if the rating was checked recently (default 7 days; tunable via
ARR_FINISHER_RATING_CACHE_TTL_DAYS) - Otherwise fetches a current rating from the right provider
- If the rating changed, renames the folder and updates the service path via API (rollback on failure)
A typical nightly sweep on a fully-cached medium library finishes in well under a minute.
Scheduling. The simple option runs daily at 3 AM as LOCAL SYSTEM:
schtasks /Create /TN "arr_finisher_nightly_sweep" `
/TR "\"<repo>\arr_finisher_sweep.bat\"" `
/SC DAILY /ST 03:00 /RL HIGHEST /RU SYSTEM /FIf your library lives on a network share, LOCAL SYSTEM can't reach mapped
drives. Either use a UNC path everywhere, or switch the task to run as your
user account:
schtasks /Create /TN "arr_finisher_nightly_sweep" `
/TR "\"<repo>\arr_finisher_sweep.bat\"" `
/SC DAILY /ST 03:00 /RU "%USERNAME%" /FTuning. Set ARR_FINISHER_RATING_CACHE_TTL_DAYS in .env to change the
freshness window. To force a full re-fetch on the next run:
python arr_finisher.py --sweep --force-refresh :: re-fetch every folder this run
python arr_finisher.py --clear-cache :: wipe the entire cache
python arr_finisher.py --refresh tt7160070 :: invalidate one IMDb ID
Rollbacks. When Sonarr/Radarr refuses a path change after the disk has
already been renamed, the script renames the folder back and records the event
in .rollbacks.log next to the script. Run python arr_finisher.py --check-rollbacks (it exits non-zero and lists any FAIL entries — disk and
service out of sync, rare, needs manual fixup).
python arr_finisher.py --validate # config + connectivity check
python arr_finisher.py --sweep # rating refresh across all roots
python arr_finisher.py --sweep --force-refresh # bypass the cache TTL and re-fetch every folder
python arr_finisher.py --sweep --dry-run # preview without touching anything
python arr_finisher.py --regenerate-shortcuts # rebuild every Links/ shortcut with current code
python arr_finisher.py --stamp-dates # encode ratings into "Date created" so Explorer sorts by rating
python arr_finisher.py --stamp-dates --dry-run # preview the date each folder would get
python arr_finisher.py --service sonarr --path "D:\TV Shows\Foo" # single folder (looks up env from Sonarr/Radarr)
python arr_finisher.py --service radarr --path "E:\Movies\Foo" --force # also rewrite icon/shortcuts/tooltip even if up to date
python arr_finisher.py --clear-cache # wipe .rating_cache.json
python arr_finisher.py --refresh tt7160070 # invalidate one IMDb ID so next sweep re-fetches
python arr_finisher.py --check-rollbacks # report unresolved FAIL entries in .rollbacks.log (exit 1 if any)
python arr_finisher.py --version # print version and exit
python arr_finisher.py --verbose # include DEBUG-level logs in this run
--validate output looks like this when everything is healthy:
=== arr_finisher 1.0.0 --validate ===
[OK ] env SONARR_API_KEY is set
[OK ] env RADARR_API_KEY is set
[OK ] env OMDB_API_KEY is set
[OK ] env SUBDL_API_KEY is set
[OK ] env OPENSUBTITLES_API_KEY is set
[OK ] module pywin32 available
[OK ] module Pillow available (11.3.0)
[OK ] icons all 9 icon files present
[OK ] http Sonarr reachable (200)
[OK ] http Radarr reachable (200)
[OK ] http IMDb GraphQL reachable
[OK ] http OMDb reachable (200)
[OK ] http Kuryana reachable (200)
[OK ] http MAL API reachable (200)
[OK ] http AniList reachable
[OK ] http SubDL reachable (200)
[OK ] http OpenSubtitles reachable (200)
16 passed, 0 failed
Explorer can't sort folders by an embedded rating — folders carry no "Rating"
property, and the [IMDb 8.8] tag sits at the end of the name where the Name
sort ignores it. But Explorer can sort by Date created, and that timestamp
is writable. So arr_finisher encodes each folder's rating into its Date-created
value (higher rating → later date). Set Explorer's view to sort by Date
created, descending, and the whole library ranks by rating — stock Explorer,
no rename, no add-on. The date is purely a sort key, not a real timestamp.
New imports get stamped automatically (toggle ENABLE_RATING_DATE_SORT). To
backfill an existing library in one offline pass — it reads the rating straight
from the folder names, no API calls:
python arr_finisher.py --stamp-dates # stamp every folder under the sweep roots
python arr_finisher.py --stamp-dates --dry-run # preview the date each folder would get
python arr_finisher.py --stamp-dates --roots "E:\Movies:radarr" # explicit root (service tag is ignored here)
Untagged folders (no rating suffix) are left untouched, so they keep their real creation date and surface at the very top when sorting descending — a handy "these still need a rating" bucket.
Resolved in this order, first hit wins:
--roots "D:\Shows:sonarr" "F:\Movies:radarr"on the CLI (the service is matched on the last colon, so Windows drive letters in the path are fine)ARR_FINISHER_SWEEP_ROOTSenv var — pipe-separatedpath:servicepairs- Auto-discovery — Sonarr
/api/v3/rootfolderand Radarr/api/v3/rootfolderare queried; their configured root folders become the sweep roots - If none of the above yield anything, the sweep refuses to run and tells you to configure roots (no hardcoded fallback paths — those were removed in 1.0.0)
Auto-discovery means you usually don't need to configure roots at all if your Sonarr / Radarr instances are healthy when the sweep runs.
| File | What it is |
|---|---|
arr_finisher.log |
Rotating log (1 MB × 3 backups). Relocate with ARR_FINISHER_LOG_DIR |
.rating_cache.json |
Per-IMDb-ID "last checked" timestamps + last known rating. Powers the sweep TTL |
.rollbacks.log |
Append-only journal of rename rollbacks. Rare but worth grepping for FAIL |
arr_finisher_setup.txt |
Sidecar that appears only when critical config is missing. Auto-deleted when fixed |
.rating_cache.json and arr_finisher_setup.txt are safe to delete by hand;
the script regenerates them as needed.
At the top of arr_finisher.py — every behavior is independently switchable
(rename, icon, shortcuts, tooltip, MDL/MAL, subtitle combo, etc.). Defaults are
sensible; flip what you don't want. ENABLE_HIDE_METADATA (default off) also
hides .nfo + extra-artwork sidecars in each folder, restoring the cleaner
Explorer view the old Creator.exe -h produced.
tests\run_tests.bat # fast unit tests (no network, ~0.3 s)
tests\run_tests.bat all # + integration tests (hits live kuryana + MAL/AniList; sets NETWORK_TESTS=1)
See CHANGELOG.md for the version history.
- maforget/Folder-Icon-Creator — the folder-icon generator this whole project is built around. Its icon-creation logic is now reimplemented natively in Python (Pillow + the shell APIs it reverse-engineered to), but none of this exists without it.
- kuryana — unofficial MyDramaList API.
- MyAnimeList API — live anime scores. Fribb/anime-lists (built on manami-project + Anime-Lists) — the TVDB/TMDB/IMDb→MAL id cross-map. AniList — keyless score fallback.
- OMDb — ratings + plot summaries.
- SubDL and OpenSubtitles — direct subtitle URL resolution for the combined Subtitle shortcut.
- Sites linked from the generated shortcuts: IMDb, Letterboxd, Trakt, Simkl, Subsource.
MIT — see LICENSE.
