Compare commits

...

7 Commits

Author SHA1 Message Date
dodofarm
07f92859e6
Merge 9659cbef2c into 4580ca0bc6 2025-09-27 22:53:18 -04:00
blacktwin
4580ca0bc6
Merge pull request #440 from JonnyWong16/save_posters
Add utility/save_posters.py
2025-09-15 09:00:31 -04:00
blacktwin
8b8a7ce05f
Merge pull request #439 from JonnyWong16/select_tmdb_poster
Update select_tmdb_poster.py to support artwork
2025-09-15 08:59:59 -04:00
JonnyWong16
c7ffce283f
Always lock poster/art when skipping 2025-04-15 14:51:35 -07:00
JonnyWong16
a53295804e
Update select_tmdb_poster.py to support artwork 2025-03-30 19:36:09 -07:00
JonnyWong16
47b828f271
Add utility/save_posters.py 2025-03-30 19:34:19 -07:00
dodofarm
9659cbef2c Changed datetime.utcnow() to datetime.now(UTC) due to deprecation 2025-01-19 01:20:52 +00:00
6 changed files with 240 additions and 30 deletions

BIN
.ropeproject/autoimport.db Normal file

Binary file not shown.

View File

@ -56,7 +56,7 @@ import sys
import json
import time
import argparse
from datetime import datetime
from datetime import UTC, datetime
from requests import Session
from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException
@ -91,7 +91,7 @@ TAUTULLI_ICON = 'https://github.com/Tautulli/Tautulli/raw/master/data/interfaces
def utc_now_iso():
"""Get current time in ISO format"""
utcnow = datetime.utcnow()
utcnow = datetime.now(UTC)
return utcnow.isoformat()

View File

@ -16,7 +16,7 @@ from __future__ import unicode_literals
from builtins import range
from builtins import object
from plexapi.server import CONFIG
from datetime import datetime, timedelta, date
from datetime import UTC, datetime, timedelta, timezone, date
from requests import Session
from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException
@ -117,7 +117,7 @@ BODY_TEXT = """\
def utc_now_iso():
"""Get current time in ISO format"""
utcnow = datetime.utcnow()
utcnow = datetime.now(UTC)
return utcnow.isoformat()
@ -467,8 +467,8 @@ if __name__ == '__main__':
TODAY = int(time.time())
DAYS = opts.days
DAYS_AGO = int(TODAY - DAYS * 24 * 60 * 60)
START_DATE = (datetime.utcfromtimestamp(DAYS_AGO).strftime("%Y-%m-%d")) # DAYS_AGO as YYYY-MM-DD
END_DATE = (datetime.utcfromtimestamp(TODAY).strftime("%Y-%m-%d")) # TODAY as YYYY-MM-DD
START_DATE = (datetime.fromtimestamp(DAYS_AGO, UTC).strftime("%Y-%m-%d")) # DAYS_AGO as YYYY-MM-DD
END_DATE = (datetime.fromtimestamp(TODAY, UTC).strftime("%Y-%m-%d")) # TODAY as YYYY-MM-DD
start_date = date(date_split(START_DATE)[0], date_split(START_DATE)[1], date_split(START_DATE)[2])
end_date = date(date_split(END_DATE)[0], date_split(END_DATE)[1], date_split(END_DATE)[2])

View File

@ -591,12 +591,12 @@ def action_show(items, selector, date, users=None):
try:
if selector == 'watched':
item = users[0].watch[item]
added_at = datetime.datetime.utcfromtimestamp(float(item.added_at)).strftime("%Y-%m-%d")
added_at = datetime.datetime.fromtimestamp(float(item.added_at), datetime.UTC).strftime("%Y-%m-%d")
size = int(item.file_size) if item.file_size else 0
sizes.append(size)
if selector == 'lastPlayed':
last_played = datetime.datetime.utcfromtimestamp(float(item.last_played)).strftime("%Y-%m-%d")
last_played = datetime.datetime.fromtimestamp(float(item.last_played)datetime.UTC).strftime("%Y-%m-%d")
print(u"\t{} added {} and last played {}\tSize: {}\n\t\tFile: {}".format(
item.title, added_at, last_played, sizeof_fmt(size), item.file))
@ -697,7 +697,7 @@ if __name__ == '__main__':
date = time.mktime(time.strptime(opts.date, "%Y-%m-%d"))
if date:
days = (datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(date))
days = (datetime.datetime.now(datetime.UTC) - datetime.datetime.fromtimestamp(date))
date_format = time.strftime("%Y-%m-%d", time.localtime(date))
date_format = '{} ({} days)'.format(date_format, days.days)
# Create a Tautulli instance

121
utility/save_posters.py Normal file
View File

@ -0,0 +1,121 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Description: Saves poster and art images 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_posters.py --library "TV Shows" --poster
* Save art for an entire library:
python save_posters.py --library "Music" --art
* Save posters and art for an entire library:
python save_posters.py --library "Movies" --poster --art
* Save posters and art for a specific media type in a library:
python save_posters.py --library "TV Shows" --libtype season --poster --art
* Save posters for a specific item:
python save_posters.py --rating_key 1234 --poster
* Save art for a specific item:
python save_posters.py --rating_key 1234 --art
* Save posters and art for a specific item:
python save_posters.py --rating_key 1234 --poster --art
'''
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):
for item in library.all(libtype=libtype, includeGuids=False):
save_item(item, poster=poster, art=art)
def save_item(item, poster=False, art=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
if poster:
save_item_poster(item, save_path)
if art:
save_item_art(item, save_path)
def save_item_poster(item, save_path):
print(f"Downloading poster for {item.title} to {save_path}")
try:
download(
url=item.posterUrl,
token=plex._token,
filename='poster.jpg',
savepath=save_path,
showstatus=True # Requires `tqdm` package
)
except Exception as e:
print(f"Failed to download poster for {item.title}: {e}")
def save_item_art(item, save_path):
print(f"Downloading art for {item.title} to {save_path}")
try:
download(
url=item.artUrl,
token=plex._token,
filename='background.jpg',
savepath=save_path,
showstatus=True # Requires `tqdm` package
)
except Exception as e:
print(f"Failed to download art for {item.title}: {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')
opts = parser.parse_args()
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
if opts.rating_key:
item = plex.fetchItem(opts.rating_key)
save_item(item, opts.poster, opts.art)
elif opts.library:
library = plex.library.section(opts.library)
save_library(library, opts.libtype, opts.poster, opts.art)
else:
print("No --rating_key or --library specified. Exiting.")

View File

@ -2,19 +2,34 @@
# -*- coding: utf-8 -*-
'''
Description: Selects the default TMDB poster if no poster is selected
or the current poster is from Gracenote.
Description: Selects the default TMDB poster and art for items in a Plex library
if no poster/art is selected or the current poster/art is from Gracenote.
Author: /u/SwiftPanda16
Requires: plexapi
Usage:
* Change the posters for an entire library:
python select_tmdb_poster.py --library "Movies"
python select_tmdb_poster.py --library "Movies" --poster
* Change the art for an entire library:
python select_tmdb_poster.py --library "Movies" --art
* Change the posters and art for an entire library:
python select_tmdb_poster.py --library "Movies" --poster --art
* Change the poster for a specific item:
python select_tmdb_poster.py --rating_key 1234
python select_tmdb_poster.py --rating_key 1234 --poster
* Change the art for a specific item:
python select_tmdb_poster.py --rating_key 1234 --art
* Change the poster and art for a specific item:
python select_tmdb_poster.py --rating_key 1234 --poster --art
* By default locked posters are skipped. To update locked posters:
python select_tmdb_poster.py --library "Movies" --include_locked
python select_tmdb_poster.py --library "Movies" --include_locked --poster --art
* To override the preferred provider:
python select_tmdb_poster.py --library "Movies" --art --art_provider "fanarttv"
Tautulli script trigger:
* Notify on recently added
@ -23,7 +38,7 @@ Tautulli script conditions:
[ Media Type | is | movie ]
Tautulli script arguments:
* Recently Added:
--rating_key {rating_key}
--rating_key {rating_key} --poster --art
'''
import argparse
@ -33,6 +48,15 @@ from plexapi.server import PlexServer
plexapi.base.USER_DONT_RELOAD_FOR_KEYS.add('fields')
# Poster and art providers to replace
REPLACE_PROVIDERS = ['gracenote', 'plex', None]
# Preferred poster and art provider to use (Note not all providers are availble for all items)
# Possible options: tmdb, tvdb, imdb, fanarttv, gracenote, plex
PREFERRED_POSTER_PROVIDER = 'tmdb'
PREFERRED_ART_PROVIDER = 'tmdb'
# ## OVERRIDES - ONLY EDIT IF RUNNING SCRIPT WITHOUT TAUTULLI ##
PLEX_URL = ''
@ -43,49 +67,114 @@ PLEX_URL = PLEX_URL or os.getenv('PLEX_URL', PLEX_URL)
PLEX_TOKEN = PLEX_TOKEN or os.getenv('PLEX_TOKEN', PLEX_TOKEN)
def select_tmdb_poster_library(library, include_locked=False):
def select_library(
library,
include_locked=False,
poster=False,
poster_provider=PREFERRED_POSTER_PROVIDER,
art=False,
art_provider=PREFERRED_ART_PROVIDER
):
for item in library.all(includeGuids=False):
# Only reload for fields
item.reload(**{k: 0 for k, v in item._INCLUDES.items()})
select_tmdb_poster_item(item, include_locked=include_locked)
select_item(
item,
include_locked=include_locked,
poster=poster,
poster_provider=poster_provider,
art=art,
art_provider=art_provider
)
def select_tmdb_poster_item(item, include_locked=False):
if item.isLocked('thumb') and not include_locked:
print(f"Locked poster for {item.title}. Skipping.")
def select_item(
item,
include_locked=False,
poster=False,
poster_provider=PREFERRED_POSTER_PROVIDER,
art=False,
art_provider=PREFERRED_ART_PROVIDER
):
print(f"{item.title} ({item.year})")
if poster:
select_poster(item, include_locked, poster_provider)
if art:
select_art(item, include_locked, art_provider)
def select_poster(item, include_locked=False, provider=PREFERRED_POSTER_PROVIDER):
print(" Checking poster...")
if item.isLocked('thumb') and not include_locked: # PlexAPI 4.5.10
print(f" - Locked poster for {item.title}. Skipping.")
return
posters = item.posters()
selected_poster = next((p for p in posters if p.selected), None)
if selected_poster is None:
print(f"WARNING: No poster selected for {item.title}.")
print(f" - WARNING: No poster selected for {item.title}.")
else:
skipping = ' Skipping.' if selected_poster.provider != 'gracenote' else ''
print(f"Poster provider is '{selected_poster.provider}' for {item.title}.{skipping}")
skip_poster = selected_poster.provider not in REPLACE_PROVIDERS
print(f" - Poster provider is '{selected_poster.provider}' for {item.title}.")
if selected_poster is None or selected_poster.provider == 'gracenote':
# Fallback to first poster if no TMDB posters are available
tmdb_poster = next((p for p in posters if p.provider == 'tmdb'), posters[0])
if posters and (selected_poster is None or selected_poster.provider in REPLACE_PROVIDERS):
# Fallback to first poster if no preferred provider posters are available
provider_poster = next((p for p in posters if p.provider == provider), posters[0])
# Selecting the poster automatically locks it
tmdb_poster.select()
print(f"Selected {tmdb_poster.provider} poster for {item.title}.")
provider_poster.select()
print(f" - Selected and locked {provider_poster.provider} poster for {item.title}.")
elif skip_poster and selected_poster:
item.lockPoster()
print(f" - Locked {selected_poster.provider} poster for {item.title}.")
def select_art(item, include_locked=False, provider=PREFERRED_ART_PROVIDER):
print(" Checking art...")
if item.isLocked('art') and not include_locked: # PlexAPI 4.5.10
print(f" - Locked art for {item.title}. Skipping.")
return
arts = item.arts()
selected_art = next((p for p in arts if p.selected), None)
if selected_art is None:
print(f" - WARNING: No art selected for {item.title}.")
else:
skip_art = selected_art.provider not in REPLACE_PROVIDERS
print(f" - Art provider is '{selected_art.provider}' for {item.title}.")
if arts and (selected_art is None or selected_art.provider in REPLACE_PROVIDERS):
# Fallback to first art if no preferred provider arts are available
provider_art = next((p for p in arts if p.provider == provider), arts[0])
# Selecting the art automatically locks it
provider_art.select()
print(f" - Selected and locked {provider_art.provider} art for {item.title}.")
elif skip_art and selected_art:
item.lockArt()
print(f" - Locked {selected_art.provider} art for {item.title}.")
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--rating_key', type=int)
parser.add_argument('--library')
parser.add_argument('--include_locked', action='store_true')
parser.add_argument('--poster', action='store_true')
parser.add_argument('--poster_provider', default=PREFERRED_POSTER_PROVIDER)
parser.add_argument('--art', action='store_true')
parser.add_argument('--art_provider', default=PREFERRED_ART_PROVIDER)
opts = parser.parse_args()
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
if opts.rating_key:
item = plex.fetchItem(opts.rating_key)
select_tmdb_poster_item(item, opts.include_locked)
select_item(item, opts.include_locked, opts.poster, opts.poster_provider, opts.art, opts.art_provider)
elif opts.library:
library = plex.library.section(opts.library)
select_tmdb_poster_library(library, opts.include_locked)
select_library(library, opts.include_locked, opts.poster, opts.poster_provider, opts.art, opts.art_provider)
else:
print("No --rating_key or --library specified. Exiting.")