Last active
September 6, 2024 07:09
-
-
Save JonnyWong16/f554f407832076919dc6864a78432db2 to your computer and use it in GitHub Desktop.
Updates all metadata in the Tautulli database after moving Plex libraries.
This file contains 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: 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_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 = True # 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.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi all, a couple of questions..
I have setup a new (second) instance of Plex on my Unraid server which is pointing at the exact same libraries as the old server. The idea here is I want decommission the old Plex and run with the new. I wish to point Tautulli at the new server but retain all the viewing data from the old server. I understand this script is what I need, but do I redirect Tautulli towards the new server before running this script?
Where/How do I run this script? Do I put it into the script window under Notification Agents? Or do I run this from the command line inside the Tautulli docker?
Thanks!