-
-
Save JonnyWong16/f554f407832076919dc6864a78432db2 to your computer and use it in GitHub Desktop.
#!/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.") |
I received error
Error retrieving Tautulli history: SQL not enabled for the API.
any ideas on how to enable it
Indeed. This is not working:
Jupiter:./update_all_metadata.py
Not dry run. Creating a backup of the Tautulli database.
Retrieving all history items from the Tautulli database...
Error retrieving Tautulli history: SQL not enabled for the API.
Exiting script...
Done.
Jupiter:grep api_sql config.ini
api_sql = 1
Jupiter:
Note: Updated my Docker container for Tautulli to V2.7.5, re-ran the script and it worked. Lots of improvement could go into this script.
Anyway, initially, this didn't work for me until I updated the Docker container for Tautulli. Then it supposedly worked but if I go to the Tautulli web page and look under libraries I see no changes. Many of the libraries say there's nothing there like under the column Total Episodes/Tracks. For example, my Movies library, which has some 7000 movies, lists that there are none.
Note I'm on Synology DSM 7.x. Suddenly my Plex database got corrupted. I tried to repair it several ways but none worked. I had to eventually delete the database and have Plex recreate it. That's when I noticed that Tautulli was messed up too. This didn't fix it.
Thanks for this. Very easy to set up. My server died for no reason and had to make a new one.
I have a handful of unsupported guid. But cannot figure out why.
Unsupported guid for 'Battlestar Galactica' in the Tautulli database [Guid: local://94409]. Skipping...
I am getting this when it gets to communicating with Plex. I have 4 Movie Libraries and it appears to fail on the first one. I am trying to merge libraries in Tautulli after I had a failure on Plex and had to reinstall. Any help would be appreciated. thanks
Retrieving all library items from the Plex server...
Scanning library: Concerts
Traceback (most recent call last):
File "update_all_metadata.py", line 214, in
main()
File "update_all_metadata.py", line 129, in main
for guid in item.guids: # Also parse tags for new Plex agents
File "/home/bcarty/.local/lib/python3.8/site-packages/plexapi/base.py", line 279, in getattribute
value = super(PlexPartialObject, self).getattribute(attr)
AttributeError: 'Movie' object has no attribute 'guids'
Worked like a charm, thanks for the script!
I had removed some media in-between shutting down my old Plex server and spinning up a new one. So when I went to merge the libraries with this script it would hang. So I added a print command to see what it was hanging on so I could manually fix it in Tautulli.
Added on line 154:
`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
print("\t{title} is NEXT!".format(
title=title))`
Thank you
Any advice on how to fix Unsupported guid
entries?
I have 14 them.
Also seeing:
No match found for 5 Tautulli items on the Plex server:
Again not sure how to fix that.
Neither really seem to be causing any sort of issue in Tautulli nor in plex.
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!
hyphesten@Debian-109-buster-64-minimal:
$ sudo python3 -m pip install plexapi=2.0.0; python_version >= "3" (from requests->plexapi)Collecting plexapi
Downloading https://files.pythonhosted.org/packages/cb/b2/a44953441fc5484ea548392daa5be6e90e2283f15e6df03b4cd8b4548432/PlexA PI-4.6.1-py3-none-any.whl (131kB)
100% |████████████████████████████████| 133kB 6.9MB/s
Collecting requests (from plexapi)
Using cached https://files.pythonhosted.org/packages/92/96/144f70b972a9c0eabbd4391ef93ccd49d0f2747f4f6a2a2738e99e5adc65/requ ests-2.26.0-py2.py3-none-any.whl
Collecting urllib3<1.27,>=1.21.1 (from requests->plexapi)
Using cached https://files.pythonhosted.org/packages/5f/64/43575537846896abac0b15c3e5ac678d787a4021e906703f1766bfb8ea11/urll ib3-1.26.6-py2.py3-none-any.whl
Collecting certifi>=2017.4.17 (from requests->plexapi)
Using cached https://files.pythonhosted.org/packages/05/1b/0a0dece0e8aa492a6ec9e4ad2fe366b511558cdc73fd3abc82ba7348e875/cert ifi-2021.5.30-py2.py3-none-any.whl
Collecting charset-normalizer
Downloading https://files.pythonhosted.org/packages/c4/1d/e6ce112f7237fc746e632e1cbdc24890cad95505c6cd4b711f4fd17f4735/chars et_normalizer-2.0.3-py3-none-any.whl
Collecting idna<4,>=2.5; python_version >= "3" (from requests->plexapi)
Using cached https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna -3.2-py3-none-any.whl
Installing collected packages: urllib3, certifi, charset-normalizer, idna, requests, plexapi
Successfully installed certifi-2021.5.30 charset-normalizer-2.0.3 idna-3.2 plexapi-4.6.1 requests-2.26.0 urllib3-1.26.6
hyphesten@Debian-109-buster-64-minimal:
$ python3 -m pip install requests=2.0.0 in /usr/local/lib/python3.7/dist-packages (from requests) (2.0.3)WARNING: Value for scheme.platlib does not match. Please report this to pypa/pip#10151
distutils: /usr/local/lib/python3.7/dist-packages
sysconfig: /usr/lib/python3.7/site-packages
WARNING: Value for scheme.purelib does not match. Please report this to pypa/pip#10151
distutils: /usr/local/lib/python3.7/dist-packages
sysconfig: /usr/lib/python3.7/site-packages
WARNING: Value for scheme.headers does not match. Please report this to pypa/pip#10151
distutils: /usr/local/include/python3.7/UNKNOWN
sysconfig: /usr/include/python3.7m/UNKNOWN
WARNING: Value for scheme.scripts does not match. Please report this to pypa/pip#10151
distutils: /usr/local/bin
sysconfig: /usr/bin
WARNING: Value for scheme.data does not match. Please report this to pypa/pip#10151
distutils: /usr/local
sysconfig: /usr
WARNING: Additional context:
user = False
home = None
root = None
prefix = None
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: requests in /usr/local/lib/python3.7/dist-packages (2.26.0)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests) (2021.5.30)
Requirement already satisfied: charset-normalizer
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests) (3.2)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests) (1.26.6)
WARNING: Value for scheme.headers does not match. Please report this to pypa/pip#10151
distutils: /home/hyphesten/.local/include/python3.7m/UNKNOWN
sysconfig: /home/hyphesten/.local/include/python3.7/UNKNOWN
WARNING: Additional context:
user = True
home = None
root = None
prefix = None
hyphesten@Debian-109-buster-64-minimal:
$ python3 update_all_metadata.py$ python3 update_all_metadata.pyDry run enabled. No changes will be made to the Tautulli database.
Retrieving all history items from the Tautulli database...
Traceback (most recent call last):
File "update_all_metadata.py", line 192, in
main()
File "update_all_metadata.py", line 70, in main
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=params).json()
File "/usr/local/lib/python3.7/dist-packages/requests/models.py", line 910, in json
return complexjson.loads(self.text, **kwargs)
File "/usr/lib/python3.7/json/init.py", line 348, in loads
return _default_decoder.decode(s)
File "/usr/lib/python3.7/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/usr/lib/python3.7/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 2 column 1 (char 1)
hyphesten@Debian-109-buster-64-minimal:
Not dry run. Creating a backup of the Tautulli database.
Retrieving all history items from the Tautulli database...
Traceback (most recent call last):
File "update_all_metadata.py", line 192, in
main()
File "update_all_metadata.py", line 70, in main
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=params).json()
File "/usr/local/lib/python3.7/dist-packages/requests/models.py", line 910, in json
return complexjson.loads(self.text, **kwargs)
File "/usr/lib/python3.7/json/init.py", line 348, in loads
return _default_decoder.decode(s)
File "/usr/lib/python3.7/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/usr/lib/python3.7/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 2 column 1 (char 1)
To be honest, i'm a noob, so i don't know how to fix this problem