Last active
August 29, 2026 22:05
-
-
Save JonnyWong16/9640557cf459896a8b7e1da8863b0485 to your computer and use it in GitHub Desktop.
Saves posters, art, logos, square art, and themes from Plex to same folder as the media files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| # -*- coding: utf-8 -*- | |
| ''' | |
| Description: Saves posters, art, logos, square art, and themes from Plex to same folder as the media files. | |
| Author: /u/SwiftPanda16 | |
| Requires: plexapi, tqdm (optional) | |
| Usage: | |
| * Save posters for an entire library: | |
| python save_resources.py --library "TV Shows" --poster | |
| * Save art for an entire library: | |
| python save_resources.py --library "Music" --art | |
| * Save posters, art, logos, and square art for an entire library: | |
| python save_resources.py --library "Movies" --poster --art --logo --squareArt | |
| * Save posters and art for a specific media type in a library: | |
| python save_resources.py --library "TV Shows" --libtype season --poster --art | |
| * Save themes for an entire library: | |
| python save_resources.py --library "TV Shows" --theme | |
| * Save posters for a specific item: | |
| python save_resources.py --rating_key 1234 --poster | |
| * Save art for a specific item: | |
| python save_resources.py --rating_key 1234 --art | |
| * Save posters, art, logos, and square art for a specific item: | |
| python save_resources.py --rating_key 1234 --poster --art --logo --squareArt | |
| * Overwrite existing images (add --overwrite flag): | |
| python save_resources.py --library "Movies" --poster --overwrite | |
| ''' | |
| import argparse | |
| from pathlib import Path | |
| from plexapi.server import PlexServer | |
| from plexapi.utils import download | |
| PLEX_URL = 'http://localhost:32400' | |
| PLEX_TOKEN = 'XXXXXXXXXXXXXXXXXXXX' | |
| # Specify the mapped docker folder paths {host: container}. Leave blank {} if non-docker. | |
| MAPPED_FOLDERS = { | |
| '/mnt/movies': '/movies', | |
| '/mnt/tvshows': '/tv', | |
| } | |
| _MAPPED_FOLDERS = {Path(host): Path(container) for host, container in MAPPED_FOLDERS.items()} | |
| def map_path(file_path): | |
| for host, container in _MAPPED_FOLDERS.items(): | |
| if container in file_path.parents: | |
| return host / file_path.relative_to(container) | |
| return file_path | |
| def save_library( | |
| library, | |
| libtype=None, | |
| poster=False, | |
| art=False, | |
| logo=False, | |
| squareArt=False, | |
| theme=False, | |
| overwrite=False | |
| ): | |
| for item in library.all(libtype=libtype, includeGuids=False): | |
| save_item( | |
| item, | |
| poster=poster, | |
| art=art, | |
| logo=logo, | |
| squareArt=squareArt, | |
| theme=theme, | |
| overwrite=overwrite | |
| ) | |
| def save_item( | |
| item, | |
| poster=False, | |
| art=False, | |
| logo=False, | |
| squareArt=False, | |
| theme=False, | |
| overwrite=False | |
| ): | |
| if hasattr(item, 'locations'): | |
| file_path = Path(item.locations[0]) | |
| else: | |
| file_path = Path(next(iter(item)).locations[0]) | |
| save_path = map_path(file_path) | |
| if save_path.is_file(): | |
| save_path = save_path.parent | |
| print(f"{item.title}{f' ({item.year})' if hasattr(item, 'year') else ''}") | |
| if poster: | |
| if item.type == 'season': | |
| filename = f'season{item.seasonNumber:02d}.jpg' | |
| else: | |
| filename = 'poster.jpg' | |
| download_resource('poster', item.posterUrl, save_path, filename, overwrite=overwrite) | |
| if art: | |
| download_resource('art', item.artUrl, save_path, 'art.jpg', overwrite=overwrite) | |
| if logo: | |
| download_resource('logo', item.logoUrl, save_path, 'logo.png', overwrite=overwrite) | |
| if squareArt: | |
| download_resource('squareArt', item.squareArtUrl, save_path, 'squareArt.jpg', overwrite=overwrite) | |
| if theme: | |
| download_resource('theme', item.themeUrl, save_path, 'theme.mp3', overwrite=overwrite) | |
| def download_resource(resource, resource_url, save_path, filename, overwrite=False): | |
| full_path = save_path / filename | |
| if not overwrite and full_path.exists(): | |
| print(f" └─ {resource.capitalize()} already exists at {full_path}. Skipping.") | |
| return | |
| if not resource_url: | |
| print(f" └─ No {resource} set. Skipping.") | |
| return | |
| print(f" └─ Downloading {full_path}") | |
| try: | |
| download( | |
| url=resource_url, | |
| token=plex._token, | |
| filename=filename, | |
| savepath=save_path, | |
| showstatus=True # Requires `tqdm` package | |
| ) | |
| except Exception as e: | |
| print(f" └─ Failed to download {resource}: {e}") | |
| if __name__ == '__main__': | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('--rating_key', type=int) | |
| parser.add_argument('--library') | |
| parser.add_argument('--libtype', choices=['movie', 'show', 'season', 'artist', 'album']) | |
| parser.add_argument('--poster', action='store_true') | |
| parser.add_argument('--art', action='store_true') | |
| parser.add_argument('--logo', action='store_true') | |
| parser.add_argument('--squareArt', action='store_true') | |
| parser.add_argument('--theme', action='store_true') | |
| parser.add_argument('--overwrite', action='store_true') | |
| opts = parser.parse_args() | |
| plex = PlexServer(PLEX_URL, PLEX_TOKEN) | |
| if opts.rating_key: | |
| item = plex.fetchItem(opts.rating_key) | |
| save_item( | |
| item, | |
| poster=opts.poster, | |
| art=opts.art, | |
| logo=opts.logo, | |
| squareArt=opts.squareArt, | |
| theme=opts.theme, | |
| overwrite=opts.overwrite | |
| ) | |
| elif opts.library: | |
| library = plex.library.section(opts.library) | |
| save_library( | |
| library, | |
| libtype=opts.libtype, | |
| poster=opts.poster, | |
| art=opts.art, | |
| logo=opts.logo, | |
| squareArt=opts.squareArt, | |
| theme=opts.theme, | |
| overwrite=opts.overwrite | |
| ) | |
| else: | |
| print("No --rating_key or --library specified. Exiting.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I had the exact same high CPU issue and didn't want to rescan huge movies library. I used AI to write some python code that scans every movie folder and creates text file with the full paths to folders that had no poster.jpg in it. The script then runs a FOR loop on the list and finds largest *.mkv, mp4 etc file in the folder, then uses plexapi to lookup the movie filename in Plex and return the Plex ratingKey that I used to call JonnyWong's script using the ratingKey argument. Instead of running his script on all movies the script is run once per missing poster (in loop) and directly targets that movie, limiting Plex changes, rescanning.
(Part 2)
If all movies in Plex are matched and have nice posters beforehand all is good. Unfortunately I had a lot of movies that had no real poster in plex, so Jonny's script dumped a poster.jpg of the generic screenshot image that plex makes when there is no poster. In a huge library its a major pain to find the movies without posters, but Jonny's script actually made it easier. Since every Plex generic screenshot is a rectangular 720 pixel wide jpg of varying (<720) heights I used a bash script that loops the movies folders and runs mediainfo on every poster.jog and spits out a list of folder paths containing a poster.jpg that is exactly 720 pixels wide and less than 720 pixels high.
I manually went through the list and matched in Plex or added my own posters so all of the missing ones were fixed.
Last step was running a bash script to loop through that same list and delete the 720 wide poster.jpg files.
Now you run your python (from up top) and Jonny's script will create poster.jpg of all the new posters you fixed.
Since the python loop w/ Jonny script only accesses missing poster.jpg I scheduled it nightly so every new movie has its own poster.jpg the next day.
Hope this helps. Thanks to Jonny's script all my posters are perfect and work across Plex, Emby and Jellyfin.