-
-
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
Has someone adapted this for music libraries? If so, please share your code.
I think I'm missing something obvious. Does this script need to be run somewhere specific? I'm getting the following error which I don't know how to resolve.
Traceback (most recent call last): File "update_all_metadata.py", line 8, in <module> from plexapi.server import PlexServer ImportError: No module named plexapi.server
I think I'm missing something obvious. Does this script need to be run somewhere specific? I'm getting the following error which I don't know how to resolve.
Traceback (most recent call last): File "update_all_metadata.py", line 8, in <module> from plexapi.server import PlexServer ImportError: No module named plexapi.server
make it executable and run it:
chmod +x update_all_metadata.py
./update_all_metadata.py
That worked for me to get past that error..
Should Tautulli be running or shut down when running this? Assume shut down but wanted to check
I think I'm missing something obvious. Does this script need to be run somewhere specific? I'm getting the following error which I don't know how to resolve.
Traceback (most recent call last): File "update_all_metadata.py", line 8, in <module> from plexapi.server import PlexServer ImportError: No module named plexapi.server
make it executable and run it:
chmod +x update_all_metadata.py ./update_all_metadata.py
That worked for me to get past that error..
I put this script in the Tautulli folder on my source machine. I tried to run it from there and got this "no module named plexapi.server" error. Then I did what you said by using the two commands you listed here... and I still get that same error. Any ideas on how to get this to run? (My source machine is a Mac, and I've migrated my Plex server over to my Synology NAS)
I think I'm missing something obvious. Does this script need to be run somewhere specific? I'm getting the following error which I don't know how to resolve.
Traceback (most recent call last): File "update_all_metadata.py", line 8, in <module> from plexapi.server import PlexServer ImportError: No module named plexapi.server
make it executable and run it:
chmod +x update_all_metadata.py ./update_all_metadata.py
That worked for me to get past that error..
I put this script in the Tautulli folder on my source machine. I tried to run it from there and got this "no module named plexapi.server" error. Then I did what you said by using the two commands you listed here... and I still get that same error. Any ideas on how to get this to run? (My source machine is a Mac, and I've migrated my Plex server over to my Synology NAS)
Ensure you have the following python modules installed, by issuing the following commands
pip install plexapi
pip install requests
pip install mock
I'm trying to make this work and it doesn't. I'm on windows 10. Tried to run it as Tautulli with a shortcut with:
"C:\Program Files (x86)\Python27\python.exe" D:\Plex\Tautulli\SCRIPT\update_all_metadata.py
as a command. It opens a cmd prompt but close as fast with not idea what's the error.
I received error
Error retrieving Tautulli history: SQL not enabled for the API.
any ideas on how to enable it
if you check the code regarding SQL on line 18-19...
NOTE: Script requires 'api_sql = 1' to be enabled in the Tautulli config.ini file.
Tautulli must be shut down before editing the config file.
so edit the config.ini file and change api_sql = 1. by default it set to 0
just in case you haven't reviewed code comments below this line, you will have to set dry run from True to False. otherwise, it will run but not save in the database. hope that helps
The script works great but I have an edge case.
I have 2 movies libraries (on the same plex server) that contains exactly the same files. Weird? Not completely, the reason behind this is to provide a feature to my users that Plex doesn’t provide, one of the library have titles and synopsis in English (for English users) the other is in French, for.. well, you get the point.
At first, I though the script will try to fix only items that are unmatched, but it is more aggressive than that and will try to fix “everything he can” and therefore try to match entries between my 2 libraries even though they still exist.
This might also be an issue where some people maintain multiple versions of the same movies in differences libraries (720p/4k) to avoid unnecessary transcoding.
Is there a way to tell the script to fix only orphan entries?
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.py
Dry 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
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!
Hey, i'm running the script and a connection to the PMS can be made. Some history titles are getting pulled from the database (not recent ones it seems) but the script aborts after trying to get all the items next. Can anybody tell me whats happening here?