Skip to content

Repository files navigation

arr_finisher

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.

arr_finisher in action — library view with poster folder icons + rating suffixes, a series folder with its Links subfolder, and the Links subfolder full of shortcuts


Quick start

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.


What it does

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

Rating source auto-detection

  • Korean content → MyDramaList (via the unofficial kuryana API)
  • AnimeMyAnimeList. 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 mean is 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.

How a single import flows

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.


Install

Prerequisites:

  • Windows (pywin32 is used to create .lnk shortcut files; folder icons use Windows shell APIs)
  • Python 3.8+
  • Pillow (installed via requirements.txt) — builds the folder.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

Configure

All config lives in .env (created from .env.example). Real OS environment variables override the file.

Required

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

Optional

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.


Run

As a Sonarr/Radarr custom script (primary use)

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.

Nightly sweep (rating-refresh safety net)

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:

  1. Looks up the IMDb ID via Sonarr/Radarr
  2. Skips if the rating was checked recently (default 7 days; tunable via ARR_FINISHER_RATING_CACHE_TTL_DAYS)
  3. Otherwise fetches a current rating from the right provider
  4. 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 /F

If 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%" /F

Tuning. 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).

Manual / one-off commands

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

Sort the library by rating in Explorer

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.

Sweep roots

Resolved in this order, first hit wins:

  1. --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)
  2. ARR_FINISHER_SWEEP_ROOTS env var — pipe-separated path:service pairs
  3. Auto-discovery — Sonarr /api/v3/rootfolder and Radarr /api/v3/rootfolder are queried; their configured root folders become the sweep roots
  4. 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.


Files arr_finisher writes next to itself

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.


Feature toggles

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

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)

Changelog

See CHANGELOG.md for the version history.

Credits

License

MIT — see LICENSE.

About

Post-import finishing touches for Sonarr/Radarr on Windows: smart rating suffix (MDL for Korean, MAL for anime, IMDb otherwise), icons, shortcuts, and Explorer tooltips.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages