2018-08-23 10:43:58 +00:00
|
|
|
#!/usr/bin/env python
|
2018-04-10 16:55:03 +00:00
|
|
|
"""
|
|
|
|
Description: Sync the watch status from one user to another. Either by user or user/libraries
|
|
|
|
Author: Blacktwin
|
|
|
|
Requires: requests, plexapi, argparse
|
|
|
|
|
|
|
|
Enabling Scripts in Tautulli:
|
|
|
|
Taultulli > Settings > Notification Agents > Add a Notification Agent > Script
|
|
|
|
|
|
|
|
Configuration:
|
|
|
|
Taultulli > Settings > Notification Agents > New Script > Configuration:
|
|
|
|
|
|
|
|
Script Name: sync_watch_status.py
|
|
|
|
Set Script Timeout: default
|
|
|
|
Description: Sync watch status
|
|
|
|
Save
|
|
|
|
|
|
|
|
Triggers:
|
|
|
|
Taultulli > Settings > Notification Agents > New Script > Triggers:
|
|
|
|
|
|
|
|
Check: Notify on Watched
|
|
|
|
Save
|
|
|
|
|
|
|
|
Conditions:
|
|
|
|
Taultulli > Settings > Notification Agents > New Script > Conditions:
|
|
|
|
|
|
|
|
Set Conditions: [{username} | {is} | {user_to_sync_from} ]
|
|
|
|
Save
|
|
|
|
|
|
|
|
Script Arguments:
|
|
|
|
Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
|
|
|
|
|
|
|
Select: Notify on Watched
|
2019-02-25 05:20:45 +00:00
|
|
|
Arguments: --ratingKey {rating_key} --userTo "Username2=Server1" "Username3=Server1" --userFrom {username}={server_name}
|
2018-04-10 16:55:03 +00:00
|
|
|
|
|
|
|
Save
|
|
|
|
Close
|
|
|
|
|
|
|
|
Example:
|
|
|
|
Set in Tautulli in script notification agent or run manually
|
|
|
|
|
2019-02-25 05:20:45 +00:00
|
|
|
sync_watch_status.py --userFrom USER1=Server --userTo USER2=Server --libraries Movies
|
2018-04-10 16:55:03 +00:00
|
|
|
- Synced watch status of {title from library} to {USER2}'s account.
|
|
|
|
|
2019-02-25 05:20:45 +00:00
|
|
|
sync_watch_status.py --userFrom USER1=Server --userTo USER2=Server USER3=Server --allLibraries
|
2018-04-10 16:55:03 +00:00
|
|
|
- Synced watch status of {title from library} to {USER2 or USER3}'s account.
|
|
|
|
|
|
|
|
Excluding;
|
|
|
|
--libraries becomes excluded if --allLibraries is set
|
2019-02-25 05:20:45 +00:00
|
|
|
sync_watch_status.py --userFrom USER1=Server --userTo USER2=Server --allLibraries --libraries Movies
|
2018-04-10 16:55:03 +00:00
|
|
|
- Shared [all libraries but Movies] with USER.
|
|
|
|
|
|
|
|
"""
|
2019-02-26 15:28:51 +00:00
|
|
|
import sys
|
2018-04-10 16:55:03 +00:00
|
|
|
import requests
|
|
|
|
import argparse
|
2019-02-25 05:20:45 +00:00
|
|
|
from plexapi.myplex import MyPlexAccount
|
2018-07-11 10:49:57 +00:00
|
|
|
from plexapi.server import PlexServer, CONFIG
|
2018-04-10 16:55:03 +00:00
|
|
|
|
2019-02-25 05:20:45 +00:00
|
|
|
|
2018-07-11 10:49:57 +00:00
|
|
|
# Using CONFIG file
|
|
|
|
PLEX_TOKEN = ''
|
2019-02-26 15:28:51 +00:00
|
|
|
TAUTULLI_URL = ''
|
|
|
|
TAUTULLI_APIKEY = ''
|
2018-09-08 20:57:30 +00:00
|
|
|
|
|
|
|
if not PLEX_TOKEN:
|
|
|
|
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', '')
|
2019-02-26 15:28:51 +00:00
|
|
|
if not TAUTULLI_URL:
|
|
|
|
TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl')
|
|
|
|
if not TAUTULLI_APIKEY:
|
|
|
|
TAUTULLI_APIKEY = CONFIG.data['auth'].get('tautulli_apikey')
|
2018-04-10 16:55:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
sess = requests.Session()
|
2018-08-23 10:44:26 +00:00
|
|
|
# Ignore verifying the SSL certificate
|
|
|
|
sess.verify = False # '/path/to/certfile'
|
|
|
|
# If verify is set to a path to a directory,
|
|
|
|
# the directory must have been processed using the c_rehash utility supplied
|
|
|
|
# with OpenSSL.
|
|
|
|
if sess.verify is False:
|
|
|
|
# Disable the warning that the request is insecure, we know that...
|
|
|
|
import urllib3
|
|
|
|
|
|
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
|
2019-02-25 05:20:45 +00:00
|
|
|
account = MyPlexAccount(PLEX_TOKEN)
|
|
|
|
|
2019-02-26 15:28:51 +00:00
|
|
|
# todo-me This is cleaned up. Should only connect to servers that are selected
|
2019-02-25 05:20:45 +00:00
|
|
|
sections_lst = []
|
|
|
|
user_servers = {}
|
|
|
|
admin_servers = {}
|
|
|
|
server_users = account.users()
|
|
|
|
user_server_dict = {'data': {}}
|
|
|
|
user_data = user_server_dict['data']
|
|
|
|
|
|
|
|
# Finding and connecting to owned servers.
|
|
|
|
print('Connecting to admin owned servers.')
|
|
|
|
for resource in account.resources():
|
|
|
|
if 'server' in [resource.provides] and resource.ownerid == 0:
|
|
|
|
server_connect = resource.connect()
|
|
|
|
admin_servers[resource.name] = server_connect
|
|
|
|
# Pull section names to check against
|
|
|
|
server_sections = [section.title for section in server_connect.library.sections()]
|
|
|
|
sections_lst += server_sections
|
|
|
|
|
|
|
|
sections_lst = list(set(sections_lst))
|
|
|
|
|
|
|
|
# Add admin account
|
|
|
|
user_data[account.title] = {'account': account,
|
|
|
|
'servers': admin_servers}
|
|
|
|
|
|
|
|
# Finding what user has access to which admin owned servers
|
|
|
|
for user in server_users:
|
|
|
|
for server in user.servers:
|
|
|
|
if admin_servers.get(server.name):
|
|
|
|
user_servers[server.name] = admin_servers.get(server.name)
|
|
|
|
if not user_data.get(user.title):
|
|
|
|
user_data[user.title] = {'account': user,
|
|
|
|
'servers': user_servers}
|
|
|
|
|
2019-02-26 15:28:51 +00:00
|
|
|
# todo-me Add Tautulli history for syncing watch statuses from Tautulli to Plex
|
|
|
|
def get_history(user_id, media_type):
|
|
|
|
# Get the user history from Tautulli.
|
|
|
|
payload = {'apikey': TAUTULLI_APIKEY,
|
|
|
|
'cmd': 'get_history',
|
|
|
|
'user_id': user_id,
|
|
|
|
'media_type': media_type}
|
|
|
|
|
|
|
|
try:
|
|
|
|
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
|
|
|
response = r.json()
|
|
|
|
res_data = response['response']['data']['data']
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
|
|
|
|
2019-02-25 05:20:45 +00:00
|
|
|
|
|
|
|
def get_account(user, server):
|
|
|
|
"""
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
user: str
|
|
|
|
User's name
|
|
|
|
server: str
|
|
|
|
Server's name
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
User server class
|
|
|
|
|
|
|
|
"""
|
|
|
|
print('Checking {} on {}'.format(user, server))
|
|
|
|
if user_server_dict['data'][user]['servers'].get(server):
|
|
|
|
user_server = user_server_dict['data'][user]['servers'].get(server)
|
|
|
|
baseurl = user_server._baseurl.split('.')
|
|
|
|
url = ''.join([baseurl[0].replace('-', '.'),
|
|
|
|
baseurl[-1].replace('direct', '')])
|
|
|
|
if user == MyPlexAccount(PLEX_TOKEN).title:
|
|
|
|
token = PLEX_TOKEN
|
|
|
|
else:
|
|
|
|
userAccount = user_server.myPlexAccount().user(user)
|
|
|
|
token = userAccount.get_token(user_server.machineIdentifier)
|
|
|
|
account = PlexServer(baseurl=url, token=token, session=sess)
|
|
|
|
return account
|
2018-04-12 19:39:19 +00:00
|
|
|
else:
|
2019-02-25 05:20:45 +00:00
|
|
|
print('{} is not shared to {}'.format(user, server))
|
|
|
|
exit()
|
2018-04-10 16:55:03 +00:00
|
|
|
|
|
|
|
|
2018-07-11 10:49:57 +00:00
|
|
|
def mark_watached(sectionFrom, accountTo, userTo):
|
2019-02-25 05:20:45 +00:00
|
|
|
"""
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
sectionFrom: class
|
|
|
|
Section class of sync from server
|
|
|
|
accountTo: class
|
|
|
|
User's server class of sync to user
|
|
|
|
|
|
|
|
"""
|
2018-07-11 10:49:57 +00:00
|
|
|
# Check sections for watched items
|
2019-02-25 05:20:45 +00:00
|
|
|
print('Marking watched...')
|
|
|
|
sectionTo = accountTo.library.section(sectionFrom.title)
|
2018-07-11 10:49:57 +00:00
|
|
|
for item in sectionFrom.search(unwatched=False):
|
|
|
|
title = item.title.encode('utf-8')
|
2019-02-25 05:20:45 +00:00
|
|
|
try:
|
|
|
|
# Check movie media type
|
|
|
|
if item.type == 'movie':
|
|
|
|
watch_check = sectionTo.get(item.title)
|
|
|
|
fetch_check = sectionTo.fetchItem(watch_check.key)
|
|
|
|
if not fetch_check.isWatched:
|
|
|
|
fetch_check.markWatched()
|
|
|
|
print('Synced watch status of {} to {}\'s account.'.format(title, userTo))
|
|
|
|
# Check show media type
|
|
|
|
elif item.type == 'show':
|
|
|
|
for episode in sectionFrom.searchEpisodes(unwatched=False, title=title):
|
|
|
|
ep_title = episode.title.encode('utf-8')
|
|
|
|
watch_check = sectionTo.get(item.title)
|
|
|
|
fetch_check = sectionTo.fetchItem(watch_check.key)
|
|
|
|
if not fetch_check.isWatched:
|
|
|
|
fetch_check.markWatched()
|
|
|
|
print('Synced watch status of {} - {} to {}\'s account.'.format(title, ep_title, userTo))
|
|
|
|
except Exception:
|
|
|
|
pass
|
2018-07-11 10:49:57 +00:00
|
|
|
|
|
|
|
|
2018-04-10 16:55:03 +00:00
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="Sync watch status from one user to others.",
|
|
|
|
formatter_class=argparse.RawTextHelpFormatter)
|
|
|
|
requiredNamed = parser.add_argument_group('required named arguments')
|
|
|
|
parser.add_argument('--libraries', nargs='*', choices=sections_lst, metavar='library',
|
|
|
|
help='Space separated list of case sensitive names to process. Allowed names are: \n'
|
|
|
|
'(choices: %(choices)s)')
|
|
|
|
parser.add_argument('--allLibraries', action='store_true',
|
|
|
|
help='Select all libraries.')
|
|
|
|
parser.add_argument('--ratingKey', nargs=1,
|
|
|
|
help='Rating key of item whose watch status is to be synced.')
|
2019-02-25 05:20:45 +00:00
|
|
|
requiredNamed.add_argument('--userFrom', metavar='user=server', required=True,
|
|
|
|
type=lambda kv: kv.split("="),
|
|
|
|
help='Select user and server to sync from')
|
|
|
|
requiredNamed.add_argument('--userTo', nargs='*', metavar='user=server', required=True,
|
|
|
|
type=lambda kv: kv.split("="),
|
|
|
|
help='Select user and server to sync to.')
|
2018-04-10 16:55:03 +00:00
|
|
|
|
|
|
|
opts = parser.parse_args()
|
|
|
|
|
|
|
|
# Defining libraries
|
|
|
|
libraries = ''
|
|
|
|
if opts.allLibraries and not opts.libraries:
|
|
|
|
libraries = sections_lst
|
|
|
|
elif not opts.allLibraries and opts.libraries:
|
|
|
|
libraries = opts.libraries
|
|
|
|
elif opts.allLibraries and opts.libraries:
|
|
|
|
# If allLibraries is used then any libraries listed will be excluded
|
|
|
|
for library in opts.libraries:
|
|
|
|
sections_lst.remove(library)
|
|
|
|
libraries = sections_lst
|
|
|
|
|
2019-02-25 05:20:45 +00:00
|
|
|
# Create Sync-From user account
|
|
|
|
plexFrom = get_account(opts.userFrom[0], opts.userFrom[1])
|
|
|
|
|
2018-04-13 01:58:53 +00:00
|
|
|
# Go through list of users
|
2018-04-10 16:55:03 +00:00
|
|
|
for user in opts.userTo:
|
2019-02-25 05:20:45 +00:00
|
|
|
plexTo = get_account(user[0], user[1])
|
2018-04-10 16:55:03 +00:00
|
|
|
if libraries:
|
2018-04-13 01:58:53 +00:00
|
|
|
# Go through Libraries
|
2018-04-10 16:55:03 +00:00
|
|
|
for library in libraries:
|
|
|
|
try:
|
|
|
|
print('Checking library: {}'.format(library))
|
2018-04-13 01:58:53 +00:00
|
|
|
# Check library for watched items
|
2018-07-11 10:49:57 +00:00
|
|
|
section = plexFrom.library.section(library)
|
2019-02-25 05:20:45 +00:00
|
|
|
mark_watached(section, plexTo, user[0])
|
2018-04-10 16:55:03 +00:00
|
|
|
except Exception as e:
|
|
|
|
if str(e).startswith('Unknown'):
|
|
|
|
print('Library ({}) does not have a watch status.'.format(library))
|
|
|
|
elif str(e).startswith('(404)'):
|
|
|
|
print('Library ({}) not shared to user: {}.'.format(library, user))
|
|
|
|
else:
|
|
|
|
print(e)
|
|
|
|
pass
|
2018-04-13 01:58:53 +00:00
|
|
|
# Check rating key from Tautulli
|
2018-04-10 16:55:03 +00:00
|
|
|
elif opts.ratingKey:
|
2019-02-25 05:20:45 +00:00
|
|
|
for key in opts.ratingKey:
|
|
|
|
item = plexTo.fetchItem(int(key))
|
|
|
|
title = item.title.encode('utf-8')
|
|
|
|
print('Syncing watch status of {} to {}\'s account.'.format(title, user[0]))
|
|
|
|
item.markWatched()
|
2018-04-10 16:55:03 +00:00
|
|
|
else:
|
2018-04-12 19:39:19 +00:00
|
|
|
print('No libraries or rating key provided.')
|