#!/usr/bin/env python # -*- coding: utf-8 -*- # # Description: Updates all metadata in the Tautulli database after moving Plex libraries. # Author: /u/SwiftPanda16 # Requires: plexapi, requests from plexapi.server import PlexServer import requests ### EDIT SETTINGS ### ## Install the required modules listed above with: ## python -m pip install plexapi ## python -m pip install requests TAUTULLI_URL = 'http://localhost:8181/tautulli' TAUTULLI_APIKEY = 'xxxxxxxxxx' PLEX_URL = 'http://localhost:32400' PLEX_TOKEN = 'xxxxxxxxxx' FALLBACK_MATCH_TITLE_YEAR = True # True or False, fallback to matching by title and year if matching by ID fails FALLBACK_MATCH_TITLE = True # True or False, fallback to matching by title ONLY if matching by title and year fails DRY_RUN = False # True to dry run without making changes to the Tautulli database, False to make changes ## CODE BELOW ## def get_id_from_guid(guid): id = None if 'imdb://' in guid: id = 'imdb://' + guid.split('imdb://')[1].split('?')[0] elif 'themoviedb://' in guid: id = 'tmdb://' + guid.split('themoviedb://')[1].split('?')[0] elif 'thetvdb://' in guid: id = 'tvdb://' + guid.split('thetvdb://')[1].split('?')[0].split('/')[0] elif 'plex://' in guid: id = 'plex://' + guid.split('plex://')[1] elif 'tmdb://' in guid or 'tvdb://' in guid or 'mbid://' in guid: id = guid return id def main(): session = requests.Session() new_key_map = {} old_key_map = {} # Check for DRY_RUN. Backup Tautulli database if needed. if DRY_RUN: print("Dry run enabled. No changes will be made to the Tautulli database.") else: print("Not dry run. Creating a backup of the Tautulli database.") params = { 'cmd': 'backup_db', 'apikey': TAUTULLI_APIKEY, } session.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=params) # Get all old items from the Tautulli database (using raw SQL) print("Retrieving all history items from the Tautulli database...") recordsFiltered = None count = 0 start = 0 while recordsFiltered is None or count < recordsFiltered: params = { 'cmd': 'get_history', 'apikey': TAUTULLI_APIKEY, 'grouping': 0, 'include_activity': 0, 'media_type': 'movie,episode,track', 'order_column': 'date', 'order_dir': 'desc', 'start': start, 'length': 1000 } r = session.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=params).json() if r['response']['result'] == 'error': print("Error retrieving Tautulli history: {}".format(r['response']['message'])) print("Exiting script...") return else: if recordsFiltered is None: recordsFiltered = r['response']['data']['recordsFiltered'] for row in r['response']['data']['data']: count += 1 if row['media_type'] not in ('movie', 'episode'): continue id = get_id_from_guid(row['guid']) if id: key = row['grandparent_rating_key'] or row['rating_key'] media_type = 'show' if row['media_type'] == 'episode' else row['media_type'] title = row['grandparent_title'] or row['title'] year = str(row['year']) old_key_map[id] = (key, media_type, title, year) else: title = row['grandparent_title'] or row['title'] print("\tUnsupported guid for '{title}' in the Tautulli database [Guid: {guid}]. Skipping...".format(title=title, guid=row['guid'])) start += 1000 # Get all new items from the Plex server print("Retrieving all library items from the Plex server...") plex = PlexServer(PLEX_URL, PLEX_TOKEN) for library in plex.library.sections(): if library.type not in ('movie', 'show') or library.agent == 'com.plexapp.agents.none': print("\tSkipping library: {title}".format(title=library.title)) continue print("\tScanning library: {title}".format(title=library.title)) for item in library.all(): id = get_id_from_guid(item.guid) if id: new_key_map[id] = (item.ratingKey, item.type, item.title, str(item.year)) else: print("\t\tUnsupported guid for '{title}' in the Plex library [Guid: {guid}]. Skipping...".format(title=item.title, guid=item.guid)) for guid in item.guids: # Also parse <Guid> tags for new Plex agents id = get_id_from_guid(guid.id) if id: new_key_map[id] = (item.ratingKey, item.type, item.title, str(item.year)) new_title_year_map = {(title, year): (id, key, media_type) for id, (key, media_type, title, year) in new_key_map.items()} new_title_map = {title: (id, key, media_type, year) for id, (key, media_type, title, year) in new_key_map.items()} # Update metadata in the Tautulli database print("{}Matching Tautulli items with Plex items...".format("(DRY RUN) " if DRY_RUN else "")) if FALLBACK_MATCH_TITLE_YEAR: print("\tUsing fallback to match by title and year.") if FALLBACK_MATCH_TITLE: print("\tUsing fallback to match by title ONLY.") if not FALLBACK_MATCH_TITLE_YEAR and not FALLBACK_MATCH_TITLE: print("\tNot using any fallback to title or year.") updated = [] no_mapping = set() for id, (old_rating_key, old_type, title, year) in old_key_map.items(): new_rating_key, new_type, _, _ = new_key_map.get(id, (None, None, None, None)) new_year = None warning_year = False if not new_rating_key and FALLBACK_MATCH_TITLE_YEAR: _, new_rating_key, new_type = new_title_year_map.get((title, year), (None, None, None)) if not new_rating_key and FALLBACK_MATCH_TITLE: _, new_rating_key, new_type, new_year = new_title_map.get(title, (None, None, None, None)) if new_rating_key: if new_rating_key != old_rating_key and new_type == old_type: if new_year is not None and new_year != year: warning_year = True updated.append((title, year, old_rating_key, new_rating_key, new_type, new_year, warning_year)) else: no_mapping.add((title, year, old_rating_key)) if updated: if not DRY_RUN: url = TAUTULLI_URL.rstrip('/') + '/api/v2' for title, year, old_rating_key, new_rating_key, new_type, new_year, warning_year in updated: params = { 'cmd': 'update_metadata_details', 'apikey': TAUTULLI_APIKEY, 'old_rating_key': old_rating_key, 'new_rating_key': new_rating_key, 'media_type': new_type } session.post(url, params=params) print("{}Updated metadata for {} items:".format("(DRY RUN) " if DRY_RUN else "", len(updated))) for title, year, old_rating_key, new_rating_key, new_type, new_year, warning_year in updated: if warning_year: print("\t{title} ({year} --> {new_year}) [Rating Key: {old} --> {new}]".format( title=title, year=year, new_year=new_year, old=old_rating_key, new=new_rating_key)) else: print("\t{title} ({year}) [Rating Key: {old} --> {new}]".format( title=title, year=year, old=old_rating_key, new=new_rating_key)) if no_mapping: print("{}No match found for {} Tautulli items on the Plex server:".format("(DRY RUN) " if DRY_RUN else "", len(no_mapping))) for title, year, old_rating_key in no_mapping: print("\t{title} ({year}) [Rating Key: {old}]".format( title=title, year=year, old=old_rating_key)) # Clear all recently added items in the Tautulli database print("{}Clearing all recently added items in the Tautulli database...".format("(DRY RUN) " if DRY_RUN else "")) if not DRY_RUN: params = { 'cmd': 'delete_recently_added', 'apikey': TAUTULLI_APIKEY } r = session.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=params).json() if r['response']['result'] == 'error': print("Error clearing the Tautulli recently added database table: {}".format(r['response']['message'])) print("Exiting script...") return print("Cleared all items from the Tautulli recently added database table.") if __name__ == "__main__": main() print("Done.")