-
-
Save JonnyWong16/9640557cf459896a8b7e1da8863b0485 to your computer and use it in GitHub Desktop.
| #!/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.") |
Hi, @JonnyWong16, I see that select_tmdb_poster.py got a revision last year so that you can now use "--art" with it to also have changed poster art. Is it possible to do the same with this script, make it work for both art and poster? Right now I have two scripts, this one "save_posters.py" and one I've called "save_arts.py" which is just "save_posters.py" but I've replaced every occurence of "poster" with "art". If possible, it would be nice to have one script that does both.
Updated with separate --poster and --art flags. I have not tested it myself.
Updated with separate
--posterand--artflags. I have not tested it myself.
I've tested it and it runs as it should, no issues and it's great do be able to save both poster and art with the same command. Thank you very much!
Edit:
Only change I've made is that I've changed "filename='background.jpg'," to "filename='art.jpg'," since there is nowhere else in the script it's referred to as "background", only "art".
Wow this script is awesome. Took several hours on a large library, but worked beautifully. The only feature its missing is the ability to add poster.jpg to newly added shows/movies without redoing the entire library (maybe an --only-missing parameter that if exists=poster.jpg, skip).
I got around the half day wait/ full load by using a bash script to search library for folders not containing poster.jpg >> list, looping that list into a plexapi command to lookup the ratingKey >> list and looping that and calling your script with rating_key parameter. Works fine unless there are weird characters in the name (and I had to remove the year as well or not found), but would be much cleaner if your script could optionally check for an existing poster.jpg. Thank you very much for your work on this, I now have your script running daily to add posters as new items arrive.
I don't understand why "Saves poster and art images from Plex to same folder as the media files." The script did run, but I really have no idea just what it did other than add to log files Now what and how would one need to change to save posters in a simple windows folder [ie]
C:\Posters??????? I want easy access to the Posters. I have some colorized versions which are hard to find poster for, and yea, there are some rather crummy colorization , and some good.
Updated script with --logo and --squareArt options, as well as a flag to overwrite existing files (the script skips existing files by default now). Background art has been changed to art.jpg instead of background.jpg.
I'm receiving the error below. I've tried it on two different movies and checked to make sure they both had squareart selected:
PS C:\Users\xxxxxxxxx\Media-Scripts\save_resources> python save_resources.py --rating_key 583323 --poster --art --logo --squareArt
The Killer That Stalked New York (1950)
└─ Downloading x:\Movies\The Killer That Stalked New York (1950)\poster.jpg
poster.jpg: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.57M/1.57M [00:00<00:00, 31.3MB/s]
└─ Downloading x:\Movies\The Killer That Stalked New York (1950)\art.jpg
art.jpg: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 439k/439k [00:00<00:00, 6.54MB/s]
└─ No logo set. Skipping.
Traceback (most recent call last):
File "C:\Users\xxxxxxxxx\Media-Scripts\save_resources\save_resources.py", line 159, in <module>
save_item(
File "C:\Users\xxxxxxxxx\Media-Scripts\save_resources\save_resources.py", line 113, in save_item
download_resource('squareArt', item.squareArtUrl, save_path, 'squareArt.jpg', overwrite=overwrite)
^^^^^^^^^^^^^^^^^
File "C:\Users\xxxxxxxxx\AppData\Local\Programs\Python\Python311\Lib\site-packages\plexapi\base.py", line 617, in __getattribute__
value = super(PlexPartialObject, self).__getattribute__(attr)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Movie' object has no attribute 'squareArtUrl'
Your version of plexapi is out-of-date.
python -m pip install -U plexapi
Love the simple fixes! Thank you!!!
As my server is running at close to 100% CPU, while it reprocesses all the assets, I was wondering...
Is it possible to have an option to compare the size of the existing file with the one to be written and NOT overwrite if the files are the same size?
The reason is that most of the time I do have the file already, because I ran this type of script. However, I then later come across a movie or tv show and decide to change one or more of its assets. Usually, I later run this script with the --overwrite and it then overwrite the many assets, even though most of them are the same. However, Plex sees the date change and put the hurt on my CPU again for several hours.
This way I could just set a task schedule for run the script weekly or so without worrying about overloading the CPU each time.
No, that is outside the scope of the script. You can modify the script if you want more sophisticated file comparisons.
Understood, thanks.
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.
As my server is running at close to 100% CPU, while it reprocesses all the assets, I was wondering...
Is it possible to have an option to compare the size of the existing file with the one to be written and NOT overwrite if the files are the same size?
The reason is that most of the time I do have the file already, because I ran this type of script. However, I then later come across a movie or tv show and decide to change one or more of its assets. Usually, I later run this script with the --overwrite and it then overwrite the many assets, even though most of them are the same. However, Plex sees the date change and put the hurt on my CPU again for several hours.
This way I could just set a task schedule for run the script weekly or so without worrying about overloading the CPU each time.
Greetings!
Came across this script and sounds like exactly what I was looking for to save some of the custom artwork I've uploaded to Plex but never saved a local copy of.
I was attempting to use the script directly through Tautulli which may be part of the problem I was running into.
After updating the script to include my docker path locations/token etc found I was running into the following error when attempting a test run. (Removed movie name)
Tautulli Notifiers :: Script returned: Downloading poster for A Movie to /mnt/user/data/media/movies/A Movie (2013)/A Movie (2013).mkv Failed to download poster for A Movie: [Errno 13] Permission denied: '/mnt/user'For my specified mapped folders I have the following.
MAPPED_FOLDERS = { '/mnt/user/data/': '/data', }I've also mapped this location to my Tautulli docker container as well (Not totally sure if necessary)
From the error of course seems to be permissions related but unsure which permission level it is looking for and it's it's truly on the /mnt/user/ level as not keen on changing the permissions on such a high level of the folder tree.
Again maybe trying to run through Tautuilli is adding some unneeded complexity or I've over looked something simple.
Thanks again for sharing such an awesome sound script, really excited to get it working!