
Refresh user list on start-up and schedule refresh every 12 hours. Handle malformed lines when reading Plex logs slightly more gracefully. Send back empty list instead of None on some plex.tv requests.
411 lines
18 KiB
Python
411 lines
18 KiB
Python
# This file is part of PlexPy.
|
|
#
|
|
# PlexPy is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# PlexPy is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from plexpy import logger, helpers, plexwatch, db
|
|
|
|
from xml.dom import minidom
|
|
from httplib import HTTPSConnection
|
|
|
|
import base64
|
|
import plexpy
|
|
|
|
def refresh_users():
|
|
logger.info("Requesting users list refresh...")
|
|
result = PlexTV().get_full_users_list()
|
|
pw_db = db.DBConnection()
|
|
|
|
if len(result) > 0:
|
|
for item in result:
|
|
control_value_dict = {"username": item['username']}
|
|
new_value_dict = {"user_id": item['user_id'],
|
|
"username": item['username'],
|
|
"thumb": item['thumb'],
|
|
"email": item['email'],
|
|
"is_home_user": item['is_home_user'],
|
|
"is_allow_sync": item['is_allow_sync'],
|
|
"is_restricted": item['is_restricted']
|
|
}
|
|
|
|
pw_db.upsert('plexpy_users', new_value_dict, control_value_dict)
|
|
|
|
logger.info("Users list refreshed.")
|
|
else:
|
|
logger.warn("Unable to refresh users list.")
|
|
|
|
|
|
class PlexTV(object):
|
|
"""
|
|
Plex.tv authentication
|
|
"""
|
|
|
|
def __init__(self, username='', password=''):
|
|
self.username = username
|
|
self.password = password
|
|
self.url = 'plex.tv'
|
|
|
|
def get_plex_auth(self):
|
|
|
|
http_handler = HTTPSConnection(self.url)
|
|
base64string = base64.encodestring('%s:%s' % (self.username, self.password)).replace('\n', '')
|
|
|
|
http_handler.request("POST",
|
|
'/users/sign_in.xml',
|
|
headers={'Content-Type': 'application/xml; charset=utf-8',
|
|
'Content-Length': '0',
|
|
'X-Plex-Device-Name': 'PlexPy',
|
|
'X-Plex-Product': 'PlexPy',
|
|
'X-Plex-Version': 'v0.1 dev',
|
|
'X-Plex-Client-Identifier': 'f0864d3531d75b19fa9204eaea456515e2502017',
|
|
'Authorization': 'Basic %s' % base64string + ":"
|
|
})
|
|
|
|
response = http_handler.getresponse()
|
|
request_status = response.status
|
|
request_body = response.read()
|
|
logger.debug(u"Plex.tv response status: %r" % request_status)
|
|
logger.debug(u"Plex.tv response headers: %r" % response.getheaders())
|
|
logger.debug(u"Plex.tv content type: %r" % response.getheader('content-type'))
|
|
logger.debug(u"Plex.tv response body: %r" % request_body)
|
|
|
|
if request_status == 201:
|
|
logger.info(u"Plex.tv connection successful.")
|
|
return request_body
|
|
elif request_status >= 400 and request_status < 500:
|
|
logger.info(u"Plex.tv request failed: %s" % response.reason)
|
|
return False
|
|
else:
|
|
logger.info(u"Plex.tv notification failed serverside.")
|
|
return False
|
|
|
|
def get_token(self):
|
|
|
|
plextv_response = self.get_plex_auth()
|
|
|
|
if plextv_response:
|
|
try:
|
|
xml_parse = minidom.parseString(helpers.latinToAscii(plextv_response))
|
|
except IOError, e:
|
|
logger.warn("Error parsing XML for Plex.tv token: %s" % e)
|
|
return False
|
|
|
|
xml_head = xml_parse.getElementsByTagName('user')
|
|
if not xml_head:
|
|
logger.warn("Error parsing XML for Plex.tv token: %s" % e)
|
|
return False
|
|
|
|
auth_token = xml_head[0].getAttribute('authenticationToken')
|
|
|
|
return auth_token
|
|
else:
|
|
return False
|
|
|
|
def get_plextv_user_data(self):
|
|
|
|
plextv_response = self.get_plex_auth()
|
|
|
|
if plextv_response:
|
|
try:
|
|
user_data = helpers.convert_xml_to_dict(plextv_response)
|
|
except IOError, e:
|
|
logger.warn("Error parsing XML for Plex.tv user data: %s" % e)
|
|
return False
|
|
|
|
return user_data
|
|
else:
|
|
return False
|
|
|
|
def get_plextv_friends(self, output_format=''):
|
|
url_command = '/api/users'
|
|
http_handler = HTTPSConnection(self.url, timeout=10)
|
|
|
|
try:
|
|
http_handler.request("GET", url_command + '?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN)
|
|
response = http_handler.getresponse()
|
|
request_status = response.status
|
|
request_content = response.read()
|
|
except IOError, e:
|
|
logger.warn(u"Failed to access friends list. %s" % e)
|
|
return None
|
|
|
|
if request_status == 200:
|
|
if output_format == 'dict':
|
|
output = helpers.convert_xml_to_dict(request_content)
|
|
elif output_format == 'json':
|
|
output = helpers.convert_xml_to_json(request_content)
|
|
else:
|
|
output = request_content
|
|
else:
|
|
logger.warn(u"Failed to access friends list. Status code %r" % request_status)
|
|
return None
|
|
|
|
return output
|
|
|
|
def get_plextv_user_details(self, output_format=''):
|
|
url_command = '/users/account'
|
|
http_handler = HTTPSConnection(self.url, timeout=10)
|
|
|
|
try:
|
|
http_handler.request("GET", url_command + '?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN)
|
|
response = http_handler.getresponse()
|
|
request_status = response.status
|
|
request_content = response.read()
|
|
except IOError, e:
|
|
logger.warn(u"Failed to access user details. %s" % e)
|
|
return None
|
|
|
|
if request_status == 200:
|
|
if output_format == 'dict':
|
|
output = helpers.convert_xml_to_dict(request_content)
|
|
elif output_format == 'json':
|
|
output = helpers.convert_xml_to_json(request_content)
|
|
else:
|
|
output = request_content
|
|
else:
|
|
logger.warn(u"Failed to access user details. Status code %r" % request_status)
|
|
return None
|
|
|
|
return output
|
|
|
|
def get_plextv_server_list(self, output_format=''):
|
|
url_command = '/pms/servers.xml'
|
|
http_handler = HTTPSConnection(self.url, timeout=10)
|
|
|
|
try:
|
|
http_handler.request("GET", url_command + '?includeLite=1&X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN)
|
|
response = http_handler.getresponse()
|
|
request_status = response.status
|
|
request_content = response.read()
|
|
except IOError, e:
|
|
logger.warn(u"Failed to access server list. %s" % e)
|
|
return None
|
|
|
|
if request_status == 200:
|
|
if output_format == 'dict':
|
|
output = helpers.convert_xml_to_dict(request_content)
|
|
elif output_format == 'json':
|
|
output = helpers.convert_xml_to_json(request_content)
|
|
else:
|
|
output = request_content
|
|
else:
|
|
logger.warn(u"Failed to access server list. Status code %r" % request_status)
|
|
return None
|
|
|
|
return output
|
|
|
|
def get_plextv_sync_lists(self, machine_id='', output_format=''):
|
|
url_command = '/servers/' + machine_id + '/sync_lists'
|
|
http_handler = HTTPSConnection(self.url, timeout=10)
|
|
|
|
try:
|
|
http_handler.request("GET", url_command + '?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN)
|
|
response = http_handler.getresponse()
|
|
request_status = response.status
|
|
request_content = response.read()
|
|
except IOError, e:
|
|
logger.warn(u"Failed to access server list. %s" % e)
|
|
return None
|
|
|
|
if request_status == 200:
|
|
if output_format == 'dict':
|
|
output = helpers.convert_xml_to_dict(request_content)
|
|
elif output_format == 'json':
|
|
output = helpers.convert_xml_to_json(request_content)
|
|
else:
|
|
output = request_content
|
|
else:
|
|
logger.warn(u"Failed to access server list. Status code %r" % request_status)
|
|
return None
|
|
|
|
return output
|
|
|
|
"""
|
|
Validate xml keys to make sure they exist and return their attribute value, return blank value is none found
|
|
"""
|
|
@staticmethod
|
|
def get_xml_attr(xml_key, attribute, return_bool=False, default_return=''):
|
|
if xml_key.getAttribute(attribute):
|
|
if return_bool:
|
|
return True
|
|
else:
|
|
return xml_key.getAttribute(attribute)
|
|
else:
|
|
if return_bool:
|
|
return False
|
|
else:
|
|
return default_return
|
|
|
|
def get_full_users_list(self):
|
|
friends_list = self.get_plextv_friends()
|
|
own_account = self.get_plextv_user_details()
|
|
users_list = []
|
|
|
|
try:
|
|
xml_parse = minidom.parseString(own_account)
|
|
except Exception, e:
|
|
logger.warn("Error parsing XML for Plex account details: %s" % e)
|
|
return []
|
|
except:
|
|
logger.warn("Error parsing XML for Plex account details.")
|
|
return []
|
|
|
|
xml_head = xml_parse.getElementsByTagName('user')
|
|
if not xml_head:
|
|
logger.warn("Error parsing XML for Plex account details.")
|
|
else:
|
|
for a in xml_head:
|
|
own_details = {"user_id": self.get_xml_attr(a, 'id'),
|
|
"username": self.get_xml_attr(a, 'username'),
|
|
"thumb": self.get_xml_attr(a, 'thumb'),
|
|
"email": self.get_xml_attr(a, 'email'),
|
|
"is_home_user": self.get_xml_attr(a, 'home'),
|
|
"is_allow_sync": None,
|
|
"is_restricted": self.get_xml_attr(a, 'restricted')
|
|
}
|
|
|
|
users_list.append(own_details)
|
|
|
|
try:
|
|
xml_parse = minidom.parseString(friends_list)
|
|
except Exception, e:
|
|
logger.warn("Error parsing XML for Plex friends list: %s" % e)
|
|
except:
|
|
logger.warn("Error parsing XML for Plex friends list.")
|
|
|
|
xml_head = xml_parse.getElementsByTagName('User')
|
|
if not xml_head:
|
|
logger.warn("Error parsing XML for Plex friends list.")
|
|
else:
|
|
for a in xml_head:
|
|
friend = {"user_id": self.get_xml_attr(a, 'id'),
|
|
"username": self.get_xml_attr(a, 'title'),
|
|
"thumb": self.get_xml_attr(a, 'thumb'),
|
|
"email": self.get_xml_attr(a, 'email'),
|
|
"is_home_user": self.get_xml_attr(a, 'home'),
|
|
"is_allow_sync": self.get_xml_attr(a, 'allowSync'),
|
|
"is_restricted": self.get_xml_attr(a, 'restricted')
|
|
}
|
|
|
|
users_list.append(friend)
|
|
|
|
return users_list
|
|
|
|
def get_synced_items(self, machine_id=None, user_id=None):
|
|
sync_list = self.get_plextv_sync_lists(machine_id)
|
|
plex_watch = plexwatch.PlexWatch()
|
|
|
|
synced_items = []
|
|
|
|
try:
|
|
xml_parse = minidom.parseString(sync_list)
|
|
except Exception, e:
|
|
logger.warn("Error parsing XML for Plex sync lists: %s" % e)
|
|
return []
|
|
except:
|
|
logger.warn("Error parsing XML for Plex sync lists.")
|
|
return []
|
|
|
|
xml_head = xml_parse.getElementsByTagName('SyncList')
|
|
|
|
if not xml_head:
|
|
logger.warn("Error parsing XML for Plex sync lists.")
|
|
else:
|
|
for a in xml_head:
|
|
client_id = self.get_xml_attr(a, 'id')
|
|
sync_device = a.getElementsByTagName('Device')
|
|
for device in sync_device:
|
|
device_user_id = self.get_xml_attr(device, 'userID')
|
|
try:
|
|
device_username = plex_watch.get_user_details(user_id=device_user_id)['username']
|
|
device_friendly_name = plex_watch.get_user_details(user_id=device_user_id)['friendly_name']
|
|
except:
|
|
device_username = ''
|
|
device_friendly_name = ''
|
|
device_name = self.get_xml_attr(device, 'name')
|
|
device_product = self.get_xml_attr(device, 'product')
|
|
device_product_version = self.get_xml_attr(device, 'productVersion')
|
|
device_platform = self.get_xml_attr(device, 'platform')
|
|
device_platform_version = self.get_xml_attr(device, 'platformVersion')
|
|
device_type = self.get_xml_attr(device, 'device')
|
|
device_model = self.get_xml_attr(device, 'model')
|
|
device_last_seen = self.get_xml_attr(device, 'lastSeenAt')
|
|
|
|
# Filter by user_id
|
|
if user_id and user_id != device_user_id:
|
|
continue
|
|
|
|
for synced in a.getElementsByTagName('SyncItems'):
|
|
sync_item = synced.getElementsByTagName('SyncItem')
|
|
for item in sync_item:
|
|
sync_id = self.get_xml_attr(item, 'id')
|
|
sync_version = self.get_xml_attr(item, 'version')
|
|
sync_root_title = self.get_xml_attr(item, 'rootTitle')
|
|
sync_title = self.get_xml_attr(item, 'title')
|
|
sync_metadata_type = self.get_xml_attr(item, 'metadataType')
|
|
sync_content_type = self.get_xml_attr(item, 'contentType')
|
|
|
|
for status in item.getElementsByTagName('Status'):
|
|
status_failure_code = self.get_xml_attr(status, 'failureCode')
|
|
status_failure = self.get_xml_attr(status, 'failure')
|
|
status_state = self.get_xml_attr(status, 'state')
|
|
status_item_count = self.get_xml_attr(status, 'itemsCount')
|
|
status_item_complete_count = self.get_xml_attr(status, 'itemsCompleteCount')
|
|
status_item_downloaded_count = self.get_xml_attr(status, 'itemsDownloadedCount')
|
|
status_item_ready_count = self.get_xml_attr(status, 'itemsReadyCount')
|
|
status_item_successful_count = self.get_xml_attr(status, 'itemsSuccessfulCount')
|
|
status_total_size = self.get_xml_attr(status, 'totalSize')
|
|
status_item_download_percent_complete = helpers.get_percent(
|
|
status_item_downloaded_count, status_item_count)
|
|
|
|
for settings in item.getElementsByTagName('MediaSettings'):
|
|
settings_audio_boost = self.get_xml_attr(settings, 'audioBoost')
|
|
settings_music_bitrate = self.get_xml_attr(settings, 'musicBitrate')
|
|
settings_photo_quality = self.get_xml_attr(settings, 'photoQuality')
|
|
settings_photo_resolution = self.get_xml_attr(settings, 'photoResolution')
|
|
settings_video_quality = self.get_xml_attr(settings, 'videoQuality')
|
|
settings_video_resolution = self.get_xml_attr(settings, 'videoResolution')
|
|
|
|
if self.get_xml_attr(item.getElementsByTagName('Location')[0], 'uri').endswith('%2Fchildren'):
|
|
clean_uri = self.get_xml_attr(item.getElementsByTagName('Location')[0], 'uri')[:-11]
|
|
else:
|
|
clean_uri = self.get_xml_attr(item.getElementsByTagName('Location')[0], 'uri')
|
|
|
|
rating_key = clean_uri.rpartition('%2F')[-1]
|
|
|
|
sync_details = {"device_name": device_name,
|
|
"platform": device_platform,
|
|
"username": device_username,
|
|
"friendly_name": device_friendly_name,
|
|
"user_id": device_user_id,
|
|
"root_title": sync_root_title,
|
|
"title": sync_title,
|
|
"metadata_type": sync_metadata_type,
|
|
"content_type": sync_content_type,
|
|
"rating_key": rating_key,
|
|
"state": status_state,
|
|
"item_count": status_item_count,
|
|
"item_complete_count": status_item_complete_count,
|
|
"item_downloaded_count": status_item_downloaded_count,
|
|
"item_downloaded_percent_complete": status_item_download_percent_complete,
|
|
"music_bitrate": settings_music_bitrate,
|
|
"photo_quality": settings_photo_quality,
|
|
"video_quality": settings_video_quality,
|
|
"total_size": status_total_size,
|
|
"failure": status_failure,
|
|
"sync_id": sync_id
|
|
}
|
|
|
|
synced_items.append(sync_details)
|
|
|
|
return synced_items |