├── .gitattributes ├── example.jpg ├── settings.ini ├── README.md └── collection_updater.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defract/TMDB-Collection-Data-Retriever/HEAD/example.jpg -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [CONFIG] 2 | enable_debug = False 3 | 4 | # Plex server settings 5 | plex_url = http://127.0.0.1:32400 6 | plex_token = 7 | 8 | # Metadata settings 9 | prefer_local_art = False 10 | poster_item_limit = 15 11 | background_item_limit = 8 12 | 13 | # TMDB settings 14 | api_key = 59bb0db203a09d2820a27734437c3bd6 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TMDB Collection Data Retriever 2 | 3 | This script collects metadata (summary and images) for your plex collection that were created based on TheMovieDB. 4 | 5 | **How does it work?** 6 | 7 | The script collects all collections from your movie library that do not have a summary yet. It does this by looking the movies within the collection. The movies have a reference to the collection on TheMovieDB. If a valid collection reference can be found it will pull the summary as well as download posters/background images (and use the same web calls as if you were manually updating it over the web interface). 8 | 9 | A sample on a small test library looks like this: 10 | 11 | ![Example](example.jpg) 12 | 13 | **Attention** 14 | 15 | Currently this has only been tested with a couple of libraries, so I would recommend to do a database backup before starting the process (even though I don't think the script can do any real harm). 16 | 17 | ## Installation 18 | 19 | ### Step 1 Configuration 20 | 21 | Add your server ip and your plex token to the setting.ini 22 | 23 | https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ 24 | 25 | Additionally you can: 26 | 1. enable/disable the preference to prefer local art (if you have a non english library and want to get images in that langauge) 27 | 2. change the limit of posters/backgrounds you want to download 28 | 29 | ### Step 2 Install requirements 30 | 31 | The following additional python libraries are used 32 | 33 | requests (https://pypi.python.org/pypi/requests/2.18.4) 34 | plexapi (https://pypi.python.org/pypi/PlexAPI/3.0.6) 35 | progress (https://pypi.python.org/pypi/progress) 36 | 37 | ### MacOS Specific 38 | 39 | Install required libararies 40 | sudo pip3 install progress plexapi requests 41 | Execute 42 | python3 collection_updater.py 43 | 44 | ## Requirements 45 | 46 | Python (tested with 3.6.3) 47 | -------------------------------------------------------------------------------- /collection_updater.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import requests 4 | import configparser 5 | import xml.etree.ElementTree as ET 6 | from plexapi.server import PlexServer 7 | from progress.bar import Bar 8 | 9 | ############################################################################################################################################ 10 | # Read values from settings.ini 11 | ############################################################################################################################################ 12 | 13 | ini_name = 'settings.ini' 14 | 15 | if(not os.path.isfile(ini_name)): 16 | print('Could not find settings.ini') 17 | 18 | ini = configparser.ConfigParser() 19 | ini.read(ini_name) 20 | config = ini['CONFIG'] 21 | 22 | enable_debug = config['enable_debug'] 23 | 24 | if enable_debug == 'True': 25 | enable_debug = True 26 | else: 27 | enable_debug = False 28 | 29 | # Plex server settings 30 | PLEX_SERVER = config['plex_url'] 31 | PLEX_TOKEN = config['plex_token'] 32 | 33 | # Metadata settings 34 | PREF_LOCAL_ART = config['prefer_local_art'] 35 | 36 | if PREF_LOCAL_ART == 'True': 37 | PREF_LOCAL_ART = True 38 | else: 39 | PREF_LOCAL_ART = False 40 | 41 | POSTER_ITEM_LIMIT = int(config['poster_item_limit']) 42 | BACKGROUND_ITEM_LIMIT = int(config['background_item_limit']) 43 | 44 | # TMDB settings 45 | TMDB_APIKEY = config['api_key'] 46 | 47 | ############################################################################################################################################ 48 | # Global variables 49 | ############################################################################################################################################ 50 | 51 | payload = {} 52 | err_list = [] 53 | 54 | HEADERS = {'X-Plex-Token' : PLEX_TOKEN } 55 | 56 | TMDB_URL = 'https://api.themoviedb.org/3' 57 | TMDB_CONFIG = '%s/configuration?api_key=%s' % (TMDB_URL, TMDB_APIKEY) 58 | TMDB_MOVIE = '%s/movie/%%s?api_key=%s&language=%%s' % (TMDB_URL, TMDB_APIKEY) 59 | TMDB_COLLECTION = '%s/collection/%%s?api_key=%s&language=%%s' % (TMDB_URL, TMDB_APIKEY) 60 | TMDB_COLLECTION_IMG = '%s/collection/%%s/images?api_key=%s' % (TMDB_URL, TMDB_APIKEY) 61 | 62 | PLEX_SUMMARY = '%s/library/sections/%%s/all?type=18&id=%%s&summary.value=%%s' % PLEX_SERVER 63 | PLEX_IMAGES = '%s/library/metadata/%%s/%%s?url=%%s' % PLEX_SERVER 64 | PLEX_COLLECTIONS = '%s/library/sections/%%s/all?type=18' % PLEX_SERVER 65 | PLEX_COLLECTIONS_ITEMS = '%s/library/metadata/%%s/children' % PLEX_SERVER 66 | 67 | ############################################################################################################################################ 68 | # main function 69 | ############################################################################################################################################ 70 | 71 | def main(): 72 | section_dict = {} 73 | 74 | tmdb_conf_dict = GetTMDBData(TMDB_CONFIG) 75 | 76 | print(''.ljust(80, '=')) 77 | print('Metadata retriever for TMDB collections') 78 | print(''.ljust(80, '=')) 79 | 80 | plex = PlexServer(PLEX_SERVER, PLEX_TOKEN) 81 | plex_sections = plex.library.sections() 82 | 83 | print('\r\nYour movie libraries are:') 84 | print(''.ljust(80, '=')) 85 | 86 | for plex_section in plex_sections: 87 | if plex_section.type != 'movie': 88 | continue 89 | 90 | print('ID: %s Name: %s' % (str(plex_section.key).ljust(4, ' '), plex_section.title)) 91 | section_dict[plex_section.key] = plex_section.title 92 | 93 | print(''.ljust(80, '=')) 94 | 95 | if len(section_dict) == 0: 96 | print('Could not find any movie libraries.') 97 | return 98 | 99 | input_sections = input('\r\nEnter a whitespace separated list of library IDs to work on (e.g: 3 5 8 13):\r\n') 100 | 101 | # remove invalid characters from user input 102 | input_sections = ''.join(i for i in input_sections if i.isdigit() or i.isspace()).split() 103 | 104 | for section_id in input_sections: 105 | 106 | # ensure that it is a valid library 107 | if section_id not in section_dict: 108 | print('%s is not a valid library id.' % section_id) 109 | continue 110 | 111 | # Get all collections of library 112 | plex_all_col_xml = GetPlexData(PLEX_COLLECTIONS % section_id) 113 | 114 | print(''.ljust(80, '=')) 115 | print('Library: %s (%s collections)' % (section_dict[section_id], len(plex_all_col_xml))) 116 | print(''.ljust(80, '=')) 117 | 118 | i = 0 119 | 120 | for plex_col_xml in plex_all_col_xml: 121 | plex_col_dict = plex_col_xml.attrib 122 | i += 1 123 | 124 | print('\r\n> %s [%s/%s]' % (plex_col_dict['title'], i, len(plex_all_col_xml)) ) 125 | 126 | # only get data for collections that have no summary yet 127 | if plex_col_dict['summary'] != '': 128 | print(' Skipping collection because summary already exists.') 129 | continue 130 | 131 | plex_col_id = plex_col_dict['ratingKey'] 132 | 133 | plex_col_mov_xml = GetPlexData(PLEX_COLLECTIONS_ITEMS % plex_col_id) 134 | tmdb_col_id, lang = GetTMDBCollectionID(plex, plex_col_mov_xml) 135 | 136 | if tmdb_col_id == -1: 137 | print(' Could not find a matching TMDB collection.') 138 | 139 | err_list.append('\r\n> %s' % plex_col_dict['title']) 140 | err_list.append(' Could not find a matching TMDB collection.') 141 | continue 142 | 143 | # get collection information 144 | tmdb_col_dict = GetTMDBData(TMDB_COLLECTION % (tmdb_col_id, lang)) 145 | 146 | plex_col_title = plex_col_dict['title'] 147 | 148 | if lang == 'en': 149 | plex_col_title = plex_col_dict['title'] + ' Collection' 150 | 151 | if tmdb_col_dict['name'] != plex_col_title: 152 | print(' Invalid collection, does not match with the TMDB collection: %s' % tmdb_col_dict['name']) 153 | 154 | err_list.append('\r\n> %s' % plex_col_title) 155 | err_list.append(' Invalid collection, does not match with the TMDB collection: %s' % tmdb_col_dict['name']) 156 | continue 157 | 158 | # get collection images 159 | tmdb_col_img_dict = GetTMDBData(TMDB_COLLECTION_IMG % (tmdb_col_id)) 160 | 161 | print(' Found a total of %s posters and %s backgrounds.' % (len(tmdb_col_img_dict['posters']), len(tmdb_col_img_dict['backdrops']) )) 162 | 163 | poster_url_list = GetImages(tmdb_col_img_dict, tmdb_conf_dict, 'posters', lang, POSTER_ITEM_LIMIT) 164 | background_url_list = GetImages(tmdb_col_img_dict, tmdb_conf_dict, 'backdrops', lang, BACKGROUND_ITEM_LIMIT) 165 | 166 | # update data in plex now 167 | 168 | # 1 change summary 169 | print(' Updating summary.') 170 | r = requests.put(PLEX_SUMMARY % (section_id, plex_col_id, tmdb_col_dict['overview']), data=payload, headers=HEADERS) 171 | 172 | # 2 upload posters 173 | UploadImagesToPlex(poster_url_list, plex_col_id, 'poster', 'posters') 174 | 175 | # 3 upload backdrops 176 | UploadImagesToPlex(background_url_list, plex_col_id, 'art', 'backgrounds') 177 | 178 | # print failed libraries again 179 | if len(err_list) > 0: 180 | print('\r\nThe following libraries could not be updated:') 181 | print(''.ljust(80, '=')) 182 | for line in err_list: 183 | print(line) 184 | 185 | err_list.clear() 186 | 187 | print('\r\nFinished updating libraries.') 188 | 189 | ############################################################################################################################################ 190 | 191 | def GetPlexData(url): 192 | r = requests.get(url, headers=HEADERS) 193 | col_movies = ET.fromstring(r.text) 194 | return col_movies 195 | 196 | ############################################################################################################################################ 197 | 198 | def GetPlexPosterUrl(plex_url): 199 | r = requests.get(plex_url, headers=HEADERS) 200 | root = ET.fromstring(r.text) 201 | 202 | for child in root: 203 | dict = child.attrib 204 | 205 | if dict['selected'] == '1': 206 | url = dict['key'] 207 | return url[url.index('?url=') + 5:] 208 | 209 | ############################################################################################################################################ 210 | 211 | def UploadImagesToPlex(url_list, plex_col_id, image_type, image_type_name): 212 | if url_list: 213 | plex_main_image = '' 214 | 215 | bar = Bar(' Uploading %s:' % image_type_name, max=len(url_list)) 216 | 217 | for background_url in url_list: 218 | #print( ' Uploading: %s' % background_url) 219 | bar.next() 220 | r = requests.post(PLEX_IMAGES % (plex_col_id, image_type + 's', background_url), data=payload, headers=HEADERS) 221 | 222 | if plex_main_image == '': 223 | plex_main_image = GetPlexPosterUrl(PLEX_IMAGES % (plex_col_id, image_type + 's', background_url)) 224 | 225 | bar.finish() 226 | 227 | # set the highest rated image as selected again 228 | r = requests.put(PLEX_IMAGES % (plex_col_id, image_type, plex_main_image), data=payload, headers=HEADERS) 229 | 230 | ############################################################################################################################################ 231 | 232 | def GetTMDBCollectionID(plex, mov_in_col_xml): 233 | for mov_in_col in mov_in_col_xml: 234 | movie = plex.fetchItem(int(mov_in_col.attrib['ratingKey'])) 235 | 236 | if enable_debug: 237 | print('Movie guid: %s' % movie.guid) 238 | 239 | if movie.guid.startswith('com.plexapp.agents.imdb://'): # Plex Movie agent 240 | match = re.search("tt[0-9]\w+", movie.guid) 241 | elif movie.guid.startswith('com.plexapp.agents.themoviedb://'): # TheMovieDB agent 242 | match = re.search("[0-9]\w+", movie.guid) 243 | 244 | if not match: 245 | continue 246 | 247 | movie_id = match.group() 248 | 249 | match = re.search("lang=[a-z]{2}", movie.guid) 250 | 251 | if match: 252 | lang = match.group()[5:] 253 | 254 | movie_dict = GetTMDBData(TMDB_MOVIE % (movie_id, lang)) 255 | 256 | if movie_dict and 'belongs_to_collection' in movie_dict and movie_dict['belongs_to_collection'] != None: 257 | col_id = movie_dict['belongs_to_collection']['id'] 258 | print(' Retrieved collection id: %s (from: %s id: %s language: %s)' % (col_id, movie.title, movie_id, lang)) 259 | return col_id, lang 260 | 261 | return -1, '' 262 | 263 | ############################################################################################################################################ 264 | 265 | def GetTMDBData(url): 266 | try: 267 | r = requests.get(url) 268 | 269 | if enable_debug: 270 | print('Requests in time limit remaining: %s' % r.headers['X-RateLimit-Remaining']) 271 | 272 | return r.json() 273 | except: 274 | print('Error fetching JSON from The Movie Database: %s' % url) 275 | 276 | ########################################################################################################################################### 277 | 278 | def GetImages(img_dict, conf_dict, type, lang, artwork_item_limit): 279 | result = [] 280 | 281 | if img_dict[type]: 282 | i = 0 283 | while i < len(img_dict[type]): 284 | poster = img_dict[type][i] 285 | 286 | # remove foreign posters 287 | if poster['iso_639_1'] is not None and poster['iso_639_1'] != 'en' and poster['iso_639_1'] != lang: 288 | del img_dict[type][i] 289 | continue 290 | 291 | # boost the score for localized posters (according to the preference) 292 | if PREF_LOCAL_ART and poster['iso_639_1'] == lang: 293 | img_dict[type][i]['vote_average'] = poster['vote_average'] + 1 294 | #poster['vote_average'] = poster['vote_average'] + 1 295 | 296 | i += 1 297 | 298 | for i, poster in enumerate(sorted(img_dict[type], key=lambda k: k['vote_average'], reverse=True)): 299 | if i >= artwork_item_limit: 300 | break 301 | else: 302 | result.append(conf_dict['images']['base_url'] + 'original' + poster['file_path']) 303 | 304 | return result 305 | 306 | ############################################################################################################################################ 307 | 308 | main() --------------------------------------------------------------------------------