A Python CLI for downloading artwork from a Plex server and saving it directly into your media folders.
It supports:
- movie posters and fanart
- TV show posters, season posters, and fanart
- music artist posters, fanart, and album covers
The goal is to keep artwork as normal image files in your library instead of leaving it buried inside Plex metadata bundles.
Plex can manage artwork automatically, but that artwork is often stored in Plex bundle directories rather than alongside your media files. This tool pulls artwork out of Plex and writes it into the media library itself as files such as:
poster.jpgfanart.jpgcover.jpg
That gives you more control over backups, portability, and manual artwork management.
It also helps lock in your chosen artwork. If a media folder contains a local poster.jpg and Plex is configured to prefer local media assets for that library, Plex will use the local poster instead of replacing it with a different automatically selected one.
The script:
- connects to your Plex server with
PLEX_URLandPLEX_TOKEN - reads metadata from a selected Plex library
- resolves each media item's folder on disk
- downloads artwork from Plex
- writes the image files directly into the media folders
There is no database. The filesystem is the final output.
Run this script on a machine that can do both of the following:
- reach your Plex server over the network
- access the actual media folders that will receive
poster.jpg,fanart.jpg, andcover.jpg
If you run it on a machine that can talk to Plex but cannot access the library folders, the script will enumerate your library successfully but skip downloads because the output paths do not exist locally.
For many setups, that means running it directly on the NAS, server, or Docker host where the media is mounted.
- Supports movies, TV shows, and music libraries
- Downloads posters and fanart with
--posterand--fanart - Lists available Plex libraries with
--list-libraries - Supports file handling modes with
--mode - Supports container-to-host path remapping for Docker-based Plex installs
- Can optionally rename music album folders to match Plex metadata
git clone https://github.com/2miles/plex-poster-downloader.git
cd plex-poster-downloaderUsing a virtual environment is recommended:
python -m venv venv
source venv/bin/activate
pip install -r requirements.txtOr install the packages directly:
pip install requests python-dotenv coloramaSet PLEX_URL to your Plex server address, for example:
http://192.168.0.2:32400
One easy way:
- Log in to Plex in a browser
- Open Developer Tools
- Open the Network tab
- Open or play a media item
- Inspect any request to
/library/... - In Headers, look under Query String Parameters or Request Headers for
X-Plex-Token
Use that value for PLEX_TOKEN in your .env file.
PLEX_URL=http://192.168.0.2:32400
PLEX_TOKEN=abc123xyz456
CONTAINER_MEDIA_PREFIX=
HOST_MEDIA_PREFIX=If Plex runs in Docker, it may report media paths from inside the container, for example:
/data/media/Movies/Your Movie (2020)
But the real path on the machine running this script might be:
/volume1/data/media/Movies/Your Movie (2020)
In that case, set:
CONTAINER_MEDIA_PREFIX=/data/media
HOST_MEDIA_PREFIX=/volume1/data/mediaThe script will replace the container prefix with the host prefix before writing files.
Leave these blank if Plex already reports paths that are valid on the machine where you run the script.
--modeControls how files are handled. Default:skip. Valid values areskip,overwrite, andadd.--libraryPlex library section ID. Default:1.--posterDownloadposter.jpg.--fanartDownloadfanart.jpg.--list-librariesPrint available Plex libraries and exit.--rename-albumsRename album folders to match Plex metadata, prompting first.--force-renameRename album folders without prompting.
skipOnly download artwork when the target file does not already exist.overwriteReplace existing files.addKeep existing files and save additional copies likeposter-1.jpgorfanart-1.jpg.
If your goal is "download only the missing artwork", use --mode=skip.
python -m plex_poster_downloader --list-librariesDownload only missing posters for the library with ID 1 (the default if --library is omitted):
python3 -m plex_poster_downloader --mode=skip --posterDownload only missing posters and fanart for library 3:
python3 -m plex_poster_downloader --mode=skip --library=3 --poster --fanartAdd extra poster and fanart files without replacing existing ones:
python3 -m plex_poster_downloader --mode=add --library=3 --poster --fanartOverwrite all existing posters and fanart in library 3:
python3 -m plex_poster_downloader --mode=overwrite --library=3 --poster --fanartFor show and season artwork to land in the right place, TV folders should follow a standard Plex layout:
TV library/
├── Show Title/
│ ├── Season 01/
│ ├── Season 02/
│ └── Specials/
└── Another Show/
Recognized season folder names include:
Season 1Season 01101
Recognized specials folder names include:
SpecialsSeason 0Season 00
If your folders use non-standard names such as S01 or Season One, season posters may be skipped.
For artist, album, and cover artwork to resolve correctly, music libraries should look roughly like this:
Music library/
├── Artist Name/
│ ├── Album Title/
│ │ ├── 01 - Track.mp3
│ │ └── ...
│ └── ...
└── Another Artist/
Check that you are running the script on a machine that can access the media folders locally.
Common causes:
- running the script on a laptop instead of the NAS/server
- stale
CONTAINER_MEDIA_PREFIX/HOST_MEDIA_PREFIXvalues - media paths reported by Plex do not match the local filesystem on that machine
Use:
--mode=skip--mode=add is for keeping the current file and adding another one such as poster-1.jpg.
Check:
PLEX_URLPLEX_TOKEN- whether the resolved media path exists on disk
- whether the user running the script can write into the media folders
This script talks directly to your Plex server and writes directly into your media library. Use caution, especially with --mode=overwrite or folder renaming. Back up important media metadata before running automation against large libraries.
Originally inspired by work from Paul Salmon (TechieGuy12):