Because why wouldn’t you build a tiny CLI for image resizing and then wrap it in config files, retries, backups, and a miniature observability stack?
JFIN (pronounced “jay-fin”, and absolutely not overthought) is a Python CLI that talks to the Jellyfin API, downloads your artwork, normalizes it, and uploads it back again — safely, cautiously, and with way more ceremony than strictly necessary.
Think: Fiat 500 engine, bolted into a full DTM race chassis. Completely unnecessary.
🚧 This is an early in-development project. Ensure that you run a complete back-up of your entire metadata library and images using Jellyfin's internal backup tool (introduced in v.10.11.0) before using this script.
Table of Contents
JFIN is a safe-by-default CLI tool that:
- Normalizes chaotic TMDb logos to a consistent canvas
- Resizes misclassified “thumb” backdrops to sane size
- Optimizes oversized user profile images
- Talks only to Jellyfin’s HTTP API (no direct filesystem poking)
- Supports dry-runs, backups, and restore helpers so it’s very hard to misuse
If your Jellyfin UI looks inconsistent across devices, this tool exists for you.
Jellyfin already does a lot of smart caching and resizing. But a few specific image types still cause pain, especially in mixed-provider setups.
📢 Want a full list of arguments of why not to use TMDb plugin's scaling options (that I spent much time thinking of)? Check out Why not just use TMDb scaling?
Logos from TheMovieDatabase come in every size and ratio imaginable:
150 x 2000,750 x 90, and everything in between- CSS skins like ElegantFin do their best, but wildly different aspect ratios still mean:
- Logos jumping up and down between titles
- Some logos stretching across half the screen, others look tiny
JFIN’s logo mode:
- Downloads logos through the Jellyfin API
- Standardizes them to a consistent canvas (default
800 x 310) - Scales and centers them on a transparent background
- Optionally:
- Only downscale
- Only upscale
- Skip padding entirely if you like your chaos centered differently
Result: logos that actually line up and behave. Your UI stops looking like a ransom collage.
The TMDb plugin likes to classify certain backdrops with text as Thumb images.
That gives you “thumbs” that are:
- Full 4K, e.g.
3840 x 2160 - Stored as-is in your metadata folder
- Resized at runtime every time a client wants a friendly size
Jellyfin handles that… but:
- Your metadata folder gets bloated
- You keep converting huge source files on-the-fly when there's a cache miss.
JFIN’s thumb mode:
- Finds item thumbs via the API (respecting libraries, item types, and filters)
- Rescales them to something sane (e.g. width
1000, preserving aspect ratio via cover+crop) - Re-uploads optimized JPEGs with configurable quality
- Often cuts your thumb storage roughly in half (results may vary; claims made are not indicative of actual performance) without you ever noticing visually.
You still get nice images — just without the “why is my metadata folder this big?” moment.
Profile images seem to dodge the normal caching pipeline.
Upload an 8MB+ selfie and Jellyfin will politely serve it on every dashboard view.
JFIN’s profile mode:
- Fetches all active users
- Grabs their profile images directly
- Normalizes them to a default
256 x 256WebP (with alpha preserved) - Re-uploads the optimized version for each user
That means:
- Faster dashboard loads
- Smaller avatar payloads
- No more accidentally using a poster-sized PNG as your profile pic
Logos look fine in isolation — the chaos only becomes obvious when you see them side-by-side, displayed on an actual device.
You can see real-world comparisons for Desktop (QHD), Tablet (WQXGA), and Mobile (20:9, 1116×2484) here:
👉 Check out the Before & After: Real-World Results
At a glance, JFIN gives you:
-
🔍 API-only discovery Talks only to Jellyfin’s HTTP API (no direct filesystem poking) with optional library filters and item types.
-
🖼 Four focused modes
logo,thumb,backdrop, andprofile— each with its own canvas, format, and scaling rules tuned for Jellyfin’s UI. -
🧪 Safe-by-default behavior
dry_run = truein the generated config, hard blocking of POST/DELETE while dry-run is on, and optional backups before any writes. -
🔧 Configurable, but not fragile
Per-mode width/height/quality, scaling guards likeno_upscale/no_downscale, optional padding for logos, timeouts, retries, and throttling. -
📝 Logging and automation friendly
CLI + file logging, optional silent mode for cron, and a design that makes it easy to run periodically or inside the Jellyfin container.
🚀 Find the full feature breakdown and details here: Feature Tour
- Jellyfin 10.11.4+
- A Jellyfin API key
- Python 3.11+
✨ A full example configuration is included as
config.example.toml.
Requires Python 3.10+.
git clone https://github.com/alexkahler/Jellyfin-Image-Normalizer.git
cd jellyfin-image-normalizer
pip install -r requirements.txtOr download the ZIP directly from GitHub.
This gives you:
src/jfin/cli.py- the CLI module entrypoint (run withpython -m jfin)src/jfin/– the overengineered engine room
You can ignore the other files in the folder if you only plan to run the script.
Set the module path once from the repo root:
# POSIX (bash/zsh/sh)
export PYTHONPATH=src# PowerShell
$env:PYTHONPATH = "src"All command examples below assume PYTHONPATH=src is set.
-
Create an API key in Jellyfin (requires admin rights)
-
Go to
JELLYFIN_URL/web/#/dashboard/keys -
Click on
New API Key -
Give the API key a name, e.g.,
JFIN, and clickCreate -
Save the API key for later use in the
config.toml
-
-
Generate a config template
python -m jfin --generate-config
-
Edit your
config.tomlAt minimum:
[server] jf_url = "https://your-jellyfin.example" jf_api_key = "YOUR_API_KEY" [api] dry_run = true # keep this true until you’re happy [backup] backup = true [modes] operations = ["logo", "thumb", "backdrop", "profile"] # or a subset
Optional:
[modes] item_types = ["Movies", "Series"] # or a subset or nothing "[]" to limit processing to certain item types. [libraries] names = ["My Favourite Movies", "Reality TV Shows"] # or nothing "[]" to process all movie and tv show libraries
-
Test connectivity (no images harmed)
python -m jfin --test-jf
Every normal run now performs this pre-flight check automatically and will exit early with a clear error if Jellyfin is down, rejecting the API key, or reporting that it is shutting down.
-
Do a full dry-run
python -m jfin --dry-run
This will:
- Discover libraries/items
- Plan all scaling operations
- Log what would be uploaded
- Not touch Jellyfin at all
💡 You can keep
dry_run = falseinconfig.tomland overwrite it using--dry-runfrom CLI for quick testing. Once you're ready, simply remove the CLI flag. -
Enable actual uploads (when you’re ready)
In
config.toml:dry_run = false
Then run:
python -m jfin
❗ Remember to use
--configif your config file is not at the default repo-root path (config.toml).Now the magic happens (plus backups, if enabled).
Normalize according to settings in config.toml:
python -m jfinOverwrite mode in config.toml and only fix logos:
python -m jfin --mode=logoOverride logo size and disable padding (for the minimalists):
python -m jfin --mode=logo \
--logo-target-size 500x200 --logo-padding noneOverride mode and fix thumbs (and resize those “4K thumbs”):
python -m jfin --mode=thumb --thumb-target-size 1000x562Normalize selected images for a single item:
python -m jfin --mode=logo|thumb|backdrop --single <item_uuid>Override mode and process profile images for a single user (profile-only):
python -m jfin --mode=profile --single some_usernameRestore logos from backups (all items for one mode):
python -m jfin --mode=logo --restoreRestore everything:
python -m jfin --restore-all- If you’re happy with totally random logo sizes
- If disk usage and avatar payloads don’t bother you at all
- If the phrase “backup-aware dry-run image normalizer for a media server” makes you physically uncomfortable
Otherwise, JFIN gives you:
- Cleaner, more consistent artwork
- Smaller, saner image assets
- The smug satisfaction of knowing your Jellyfin is very slightly more polished than it needs to be
JFIN includes a “backup” and “restore” function, but these are not real backups in the Jellyfin sense.
Short version:
- JFIN only backs up the image files it processed — nothing more, nothing less.
- It does not back up Jellyfin’s database, item UUID mappings, watch history, playlists, subtitles, chapters, or anything outside the specific images JFIN touched.
- If Jellyfin re-imports your media and UUIDs change, JFIN cannot restore images to items that no longer exist under the same ID.
Jellyfin 10.11+ provides a robust, official, database-aware backup + restore. Use that for real server recovery, and treat JFIN’s backups as a convenience utility only.
❗ Full details and edge cases: Backups, UUIDs, and what JFIN can’t restore
Additional documentation lives in the docs/ folder:
- Before & After comparisons
- Why not just use TMDb scaling?
- Feature tour (full details)
- Backups, UUIDs, and restore limitations
- Advanced usage & tips
- Cron, Docker, and LSIO container setups
- Technical notes and internals
Contributions, bug reports, and “why did you build this?” issues are all welcome.
If you want to hack on JFIN:
- Fork the repo
- Make your changes in a feature branch
- Add or update tests where it makes sense
- Run the test suite (see below)
- Open a pull request
For internal architecture and layout, see: Technical Notes.
- Code lives in
src/jfin/ - CLI entry is
python -m jfin(src/jfin/cli.py) - Dependencies are listed in
requirements.txt
Run tests:
PYTHONPATH=src python -m pytestMore details about the processing pipeline, classes, config TOML, and CLI entrypoints are documented in:
This project is licensed under GPL-3.0. See the LICENSE file for details.