diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index dafae876..682daabe 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -653,7 +653,7 @@
- +
diff --git a/plexpy/__init__.py b/plexpy/__init__.py index ec243428..a2c473e6 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -36,12 +36,14 @@ import activity_handler import activity_pinger import config import database +import libraries import logger import mobile_app import notification_handler import notifiers import plextv import pmsconnect +import users import versioncheck import plexpy.config @@ -213,16 +215,15 @@ def initialize(config_file): # Get the real PMS urls for SSL and remote access if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT: - plextv.get_real_pms_url() - pmsconnect.get_server_friendly_name() + plextv.get_server_resources() # Refresh the users list on startup if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP: - plextv.refresh_users() + users.refresh_users() # Refresh the libraries list on startup if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP: - pmsconnect.refresh_libraries() + libraries.refresh_libraries() # Store the original umask UMASK = os.umask(0) @@ -323,14 +324,8 @@ def initialize_scheduler(): hours=backup_hours, minutes=0, seconds=0, args=(True, True)) if WS_CONNECTED and CONFIG.PMS_IP and CONFIG.PMS_TOKEN: - #schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions', - # hours=0, minutes=0, seconds=1) - #schedule_job(activity_pinger.check_recently_added, 'Check for recently added items', - # hours=0, minutes=0, seconds=monitor_seconds * bool(CONFIG.NOTIFY_RECENTLY_ADDED)) - schedule_job(plextv.get_real_pms_url, 'Refresh Plex server URLs', + schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs', hours=12 * (not bool(CONFIG.PMS_URL_MANUAL)), minutes=0, seconds=0) - schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex server name', - hours=12, minutes=0, seconds=0) schedule_job(activity_pinger.check_server_access, 'Check for Plex remote access', hours=0, minutes=0, seconds=60 * bool(CONFIG.MONITOR_REMOTE_ACCESS)) @@ -341,9 +336,9 @@ def initialize_scheduler(): user_hours = CONFIG.REFRESH_USERS_INTERVAL if 1 <= CONFIG.REFRESH_USERS_INTERVAL <= 24 else 12 library_hours = CONFIG.REFRESH_LIBRARIES_INTERVAL if 1 <= CONFIG.REFRESH_LIBRARIES_INTERVAL <= 24 else 12 - schedule_job(plextv.refresh_users, 'Refresh users list', + schedule_job(users.refresh_users, 'Refresh users list', hours=user_hours, minutes=0, seconds=0) - schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list', + schedule_job(libraries.refresh_libraries, 'Refresh libraries list', hours=library_hours, minutes=0, seconds=0) schedule_job(activity_pinger.check_server_response, 'Check server response', @@ -351,9 +346,7 @@ def initialize_scheduler(): else: # Cancel all jobs - schedule_job(plextv.get_real_pms_url, 'Refresh Plex server URLs', - hours=0, minutes=0, seconds=0) - schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex server name', + schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs', hours=0, minutes=0, seconds=0) schedule_job(activity_pinger.check_server_access, 'Check for Plex remote access', @@ -361,9 +354,9 @@ def initialize_scheduler(): schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates', hours=0, minutes=0, seconds=0) - schedule_job(plextv.refresh_users, 'Refresh users list', + schedule_job(users.refresh_users, 'Refresh users list', hours=0, minutes=0, seconds=0) - schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list', + schedule_job(libraries.refresh_libraries, 'Refresh libraries list', hours=0, minutes=0, seconds=0) # Schedule job to reconnect websocket diff --git a/plexpy/activity_pinger.py b/plexpy/activity_pinger.py index 373b8b80..ae7341bf 100644 --- a/plexpy/activity_pinger.py +++ b/plexpy/activity_pinger.py @@ -278,7 +278,6 @@ def check_server_response(): def check_server_access(): - with monitor_lock: pms_connect = pmsconnect.PmsConnect() server_response = pms_connect.get_server_response() @@ -287,7 +286,7 @@ def check_server_access(): # Check for remote access if server_response: - + mapping_state = server_response['mapping_state'] mapping_error = server_response['mapping_error'] diff --git a/plexpy/api2.py b/plexpy/api2.py index 5355c904..4706daa7 100644 --- a/plexpy/api2.py +++ b/plexpy/api2.py @@ -32,10 +32,10 @@ import xmltodict import plexpy import config import database +import libraries import logger import mobile_app -import plextv -import pmsconnect +import users class API2: @@ -345,14 +345,14 @@ class API2: def refresh_libraries_list(self, **kwargs): """ Refresh the Tautulli libraries list.""" - data = pmsconnect.refresh_libraries() + data = libraries.refresh_libraries() self._api_result_type = 'success' if data else 'error' return data def refresh_users_list(self, **kwargs): """ Refresh the Tautulli users list.""" - data = plextv.refresh_users() + data = users.refresh_users() self._api_result_type = 'success' if data else 'error' return data diff --git a/plexpy/common.py b/plexpy/common.py index f88c217d..d3618a24 100644 --- a/plexpy/common.py +++ b/plexpy/common.py @@ -140,10 +140,10 @@ SCHEDULER_LIST = ['Check GitHub for updates', 'Check for recently added items', 'Check for Plex updates', 'Check for Plex remote access', + 'Check server response', 'Refresh users list', 'Refresh libraries list', 'Refresh Plex server URLs', - 'Refresh Plex server name', 'Backup Tautulli database', 'Backup Tautulli config' ] diff --git a/plexpy/config.py b/plexpy/config.py index 16f64796..10308c22 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -45,6 +45,7 @@ _CONFIG_DEFINITIONS = { 'PLEXWATCH_DATABASE': (str, 'PlexWatch', ''), 'PMS_IDENTIFIER': (str, 'PMS', ''), 'PMS_IP': (str, 'PMS', '127.0.0.1'), + 'PMS_IS_CLOUD': (int, 'PMS', 0), 'PMS_IS_REMOTE': (int, 'PMS', 0), 'PMS_LOGS_FOLDER': (str, 'PMS', ''), 'PMS_LOGS_LINE_CAP': (int, 'PMS', 1000), diff --git a/plexpy/http_handler.py b/plexpy/http_handler.py index 223a65e1..966adb54 100644 --- a/plexpy/http_handler.py +++ b/plexpy/http_handler.py @@ -1,24 +1,28 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# This file is part of Tautulli. +# This file is part of PlexPy. # -# Tautulli is free software: you can redistribute it and/or modify +# 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. # -# Tautulli is distributed in the hope that it will be useful, +# 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 Tautulli. If not, see . +# along with PlexPy. If not, see . -from httplib import HTTPSConnection -from httplib import HTTPConnection -import ssl +from functools import partial +from multiprocessing.dummy import Pool as ThreadPool +from urlparse import urljoin + +import certifi +from requests.packages import urllib3 +from requests.packages.urllib3.exceptions import InsecureRequestWarning import plexpy import helpers @@ -30,94 +34,144 @@ class HTTPHandler(object): Retrieve data from Plex Server """ - def __init__(self, host, port, token, ssl_verify=True): - self.host = host - self.port = str(port) + def __init__(self, urls, token=None, timeout=10, ssl_verify=True): + if isinstance(urls, basestring): + self.urls = urls.split() or urls.split(',') + else: + self.urls = urls + self.token = token + if self.token: + self.headers = {'X-Plex-Token': self.token} + else: + self.headers = {} + + self.timeout = timeout self.ssl_verify = ssl_verify - """ - Handle the HTTP requests. + self.valid_request_types = ('GET', 'POST', 'PUT', 'DELETE') - Output: object - """ def make_request(self, - uri=None, proto='HTTP', - request_type='GET', + uri=None, headers=None, + request_type='GET', output_format='raw', return_type=False, no_token=False, - timeout=None): + timeout=None, + callback=None): + """ + Handle the HTTP requests. - if timeout is None: - timeout = plexpy.CONFIG.PMS_TIMEOUT + Output: list + """ - valid_request_types = ['GET', 'POST', 'PUT', 'DELETE'] + self.uri = uri + self.request_type = request_type.upper() + self.output_format = output_format.lower() + self.return_type = return_type + self.callback = callback + self.timeout = timeout or self.timeout - if request_type.upper() not in valid_request_types: + if self.request_type not in self.valid_request_types: logger.debug(u"HTTP request made but unsupported request type given.") return None if uri: - if proto.upper() == 'HTTPS': - if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'): - context = ssl._create_unverified_context() - handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout, context=context) - logger.warn(u"Tautulli HTTP Handler :: Unverified HTTPS request made. This connection is not secure.") - else: - handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout) - else: - handler = HTTPConnection(host=self.host, port=self.port, timeout=timeout) + request_urls = [urljoin(url, self.uri) for url in self.urls] - if not no_token: - if headers: - headers.update({'X-Plex-Token': self.token}) - else: - headers = {'X-Plex-Token': self.token} + if no_token and headers: + self.headers = headers + elif headers: + self.headers.update(headers) - try: - if headers: - handler.request(request_type, uri, headers=headers) - else: - handler.request(request_type, uri) - response = handler.getresponse() - request_status = response.status - request_content = response.read() - content_type = response.getheader('content-type') - except IOError as e: - logger.warn(u"Failed to access uri endpoint %s with error %s" % (uri, e)) - return None - except Exception as e: - logger.warn(u"Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only? %s" % (uri, e)) - return None - except: - logger.warn(u"Failed to access uri endpoint %s with Uncaught exception." % uri) - return None + responses = [] + for r in self._http_requests_pool(request_urls): + responses.append(r) - if request_status in (200, 201): - try: - if output_format == 'dict': - output = helpers.convert_xml_to_dict(request_content) - elif output_format == 'json': - output = helpers.convert_xml_to_json(request_content) - elif output_format == 'xml': - output = helpers.parse_xml(request_content) - else: - output = request_content + return responses[0] - if return_type: - return output, content_type - - return output - - except Exception as e: - logger.warn(u"Failed format response from uri %s to %s error %s" % (uri, output_format, e)) - return None - - else: - logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (uri, request_status)) - return None else: logger.debug(u"HTTP request made but no enpoint given.") return None + + def _http_requests_pool(self, urls, workers=10, chunk=None): + """Generator function to request urls in chunks""" + # From cpython + if chunk is None: + chunk, extra = divmod(len(urls), workers * 4) + if extra: + chunk += 1 + if len(urls) == 0: + chunk = 0 + + if self.ssl_verify: + session = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where()) + else: + urllib3.disable_warnings(InsecureRequestWarning) + session = urllib3.PoolManager() + part = partial(self._http_requests_urllib3, session=session) + + if len(urls) == 1: + yield part(urls[0]) + else: + pool = ThreadPool(workers) + + try: + for work in pool.imap_unordered(part, urls, chunk): + yield work + except Exception as e: + logger.error(u"Failed to yield request: %s" % e) + finally: + pool.close() + pool.join() + + def _http_requests_urllib3(self, url, session): + """Request the data from the url""" + try: + r = session.request(self.request_type, url, headers=self.headers, timeout=self.timeout) + except IOError as e: + logger.warn(u"Failed to access uri endpoint %s with error %s" % (self.uri, e)) + return None + except Exception as e: + logger.warn(u"Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only? %s" % (self.uri, e)) + return None + except: + logger.warn(u"Failed to access uri endpoint %s with Uncaught exception." % self.uri) + return None + + response_status = r.status + response_content = r.data + response_headers = r.headers + + if response_status in (200, 201): + return self._http_format_output(response_content, response_headers) + else: + logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (self.uri, response_status)) + return None + + def _http_format_output(self, response_content, response_headers): + """Formats the request response to the desired type""" + try: + if self.output_format == 'text': + output = response_content.decode('utf-8', 'ignore') + if self.output_format == 'dict': + output = helpers.convert_xml_to_dict(response_content.decode('utf-8', 'ignore')) + elif self.output_format == 'json': + output = helpers.convert_xml_to_json(response_content.decode('utf-8', 'ignore')) + elif self.output_format == 'xml': + output = helpers.parse_xml(response_content.decode('utf-8', 'ignore')) + else: + output = response_content + + if self.callback: + return self.callback(output) + + if self.return_type: + return output, response_headers['Content-Type'] + + return output + + except Exception as e: + logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.response_type, e)) + return None diff --git a/plexpy/libraries.py b/plexpy/libraries.py index 2bc007ab..d43e1309 100644 --- a/plexpy/libraries.py +++ b/plexpy/libraries.py @@ -27,6 +27,66 @@ import pmsconnect import session +def refresh_libraries(): + logger.info(u"Tautulli Libraries :: Requesting libraries list refresh...") + + server_id = plexpy.CONFIG.PMS_IDENTIFIER + if not server_id: + logger.error(u"Tautulli Libraries :: No PMS identifier, cannot refresh libraries. Verify server in settings.") + return + + library_sections = pmsconnect.PmsConnect().get_library_details() + + if library_sections: + monitor_db = database.MonitorDatabase() + + library_keys = [] + new_keys = [] + + for section in library_sections: + section_keys = {'server_id': server_id, + 'section_id': section['section_id']} + section_values = {'server_id': server_id, + 'section_id': section['section_id'], + 'section_name': section['section_name'], + 'section_type': section['section_type'], + 'thumb': section['thumb'], + 'art': section['art'], + 'count': section['count'], + 'parent_count': section.get('parent_count', None), + 'child_count': section.get('child_count', None), + } + + result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values) + + library_keys.append(section['section_id']) + + if result == 'insert': + new_keys.append(section['section_id']) + + if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']: + plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys) + plexpy.CONFIG.write() + else: + new_keys = plexpy.CONFIG.HOME_LIBRARY_CARDS + new_keys + plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys) + plexpy.CONFIG.write() + + #if plexpy.CONFIG.UPDATE_SECTION_IDS == 1 or plexpy.CONFIG.UPDATE_SECTION_IDS == -1: + # # Start library section_id update on it's own thread + # threading.Thread(target=libraries.update_section_ids).start() + + #if plexpy.CONFIG.UPDATE_LABELS == 1 or plexpy.CONFIG.UPDATE_LABELS == -1: + # # Start library labels update on it's own thread + # threading.Thread(target=libraries.update_labels).start() + + logger.info(u"Tautulli Libraries :: Libraries list refreshed.") + return True + else: + logger.warn(u"Tautulli Libraries :: Unable to refresh libraries list.") + return False + + def update_section_ids(): plexpy.CONFIG.UPDATE_SECTION_IDS = -1 @@ -965,7 +1025,7 @@ class Libraries(object): monitor_db = database.MonitorDatabase() # Refresh the PMS_URL to make sure the server_id is updated - plextv.get_real_pms_url() + plextv.get_server_resources() server_id = plexpy.CONFIG.PMS_IDENTIFIER diff --git a/plexpy/plexivity_import.py b/plexpy/plexivity_import.py index 226dfbc7..cdc1186b 100644 --- a/plexpy/plexivity_import.py +++ b/plexpy/plexivity_import.py @@ -23,7 +23,6 @@ import activity_processor import database import helpers import logger -import plextv import users @@ -284,7 +283,7 @@ def import_from_plexivity(database=None, table_name=None, import_ignore_interval # Get the latest friends list so we can pull user id's try: - plextv.refresh_users() + users.refresh_users() except: logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.") return None diff --git a/plexpy/plextv.py b/plexpy/plextv.py index 3782fc1c..7f75eb27 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -18,11 +18,9 @@ import base64 import json -from xml.dom import minidom import plexpy import common -import database import helpers import http_handler import logger @@ -31,129 +29,99 @@ import pmsconnect import session -def refresh_users(): - logger.info(u"Tautulli PlexTV :: Requesting users list refresh...") - result = PlexTV().get_full_users_list() +def get_server_resources(return_presence=False): + if not return_presence: + logger.info(u"Tautulli PlexTV :: Requesting resources for server...") - monitor_db = database.MonitorDatabase() - user_data = users.Users() + server = {'pms_name': plexpy.CONFIG.PMS_NAME, + 'pms_version': plexpy.CONFIG.PMS_VERSION, + 'pms_platform': plexpy.CONFIG.PMS_PLATFORM, + 'pms_ip': plexpy.CONFIG.PMS_IP, + 'pms_port': plexpy.CONFIG.PMS_PORT, + 'pms_ssl': plexpy.CONFIG.PMS_SSL, + 'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE, + 'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD, + 'pms_url': plexpy.CONFIG.PMS_URL, + 'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL + } - if result: - for item in result: - - shared_libraries = '' - user_tokens = user_data.get_tokens(user_id=item['user_id']) - if user_tokens and user_tokens['server_token']: - pms_connect = pmsconnect.PmsConnect(token=user_tokens['server_token']) - library_details = pms_connect.get_server_children() - - if library_details: - shared_libraries = ';'.join(d['section_id'] for d in library_details['libraries_list']) - else: - shared_libraries = '' - - control_value_dict = {"user_id": item['user_id']} - new_value_dict = {"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'], - "shared_libraries": shared_libraries, - "filter_all": item['filter_all'], - "filter_movies": item['filter_movies'], - "filter_tv": item['filter_tv'], - "filter_music": item['filter_music'], - "filter_photos": item['filter_photos'] - } - - # Check if we've set a custom avatar if so don't overwrite it. - if item['user_id']: - avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url ' - 'FROM users WHERE user_id = ?', - [item['user_id']]) - if avatar_urls: - if not avatar_urls[0]['custom_avatar_url'] or \ - avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']: - new_value_dict['custom_avatar_url'] = item['thumb'] - else: - new_value_dict['custom_avatar_url'] = item['thumb'] - - monitor_db.upsert('users', new_value_dict, control_value_dict) - - logger.info(u"Tautulli PlexTV :: Users list refreshed.") - return True + if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']: + scheme = 'https' else: - logger.warn(u"Tautulli PlexTV :: Unable to refresh users list.") - return False + scheme = 'http' - -def get_real_pms_url(): - logger.info(u"Tautulli PlexTV :: Requesting URLs for server...") - - # Reset any current PMS_URL value - plexpy.CONFIG.__setattr__('PMS_URL', '') - plexpy.CONFIG.write() - - fallback_url = 'http://{}:{}'.format(plexpy.CONFIG.PMS_IP, plexpy.CONFIG.PMS_PORT) + fallback_url = '{scheme}://{hostname}:{port}'.format(scheme=scheme, + hostname=server['pms_ip'], + port=server['pms_port']) plex_tv = PlexTV() - result = plex_tv.get_server_urls(include_https=plexpy.CONFIG.PMS_SSL) - plexpass = plex_tv.get_plexpass_status() + result = plex_tv.get_server_connections(pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER, + pms_ip=server['pms_ip'], + pms_port=server['pms_port'], + include_https=server['pms_ssl']) - connections = [] if result: - plexpy.CONFIG.__setattr__('PMS_VERSION', result['version']) - plexpy.CONFIG.__setattr__('PMS_PLATFORM', result['platform']) - plexpy.CONFIG.__setattr__('PMS_PLEXPASS', plexpass) - connections = result['connections'] + connections = result.pop('connections', []) + server.update(result) + presence = server.pop('server_presence', 0) + else: + connections = [] + presence = 0 + + if return_presence: + return presence + + plexpass = plex_tv.get_plexpass_status() + server['pms_plexpass'] = int(plexpass) # Only need to retrieve PMS_URL if using SSL - if not plexpy.CONFIG.PMS_URL_MANUAL and plexpy.CONFIG.PMS_SSL: + if not server['pms_url_manual'] and server['pms_ssl']: if connections: - if plexpy.CONFIG.PMS_IS_REMOTE: + if server['pms_is_remote']: # Get all remote connections - conns = [c for c in connections if c['local'] == '0' and 'plex.direct' in c['uri']] + conns = [c for c in connections if + c['local'] == '0' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])] else: # Get all local connections - conns = [c for c in connections if c['local'] == '1' and 'plex.direct' in c['uri']] + conns = [c for c in connections if + c['local'] == '1' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])] if conns: # Get connection with matching address, otherwise return first connection - conn = next((c for c in conns if c['address'] == plexpy.CONFIG.PMS_IP - and c['port'] == str(plexpy.CONFIG.PMS_PORT)), conns[0]) - plexpy.CONFIG.__setattr__('PMS_URL', conn['uri']) - plexpy.CONFIG.write() + conn = next((c for c in conns if c['address'] == server['pms_ip'] + and c['port'] == str(server['pms_port'])), conns[0]) + server['pms_url'] = conn['uri'] logger.info(u"Tautulli PlexTV :: Server URL retrieved.") # get_server_urls() failed or PMS_URL not found, fallback url doesn't use SSL - if not plexpy.CONFIG.PMS_URL: - plexpy.CONFIG.__setattr__('PMS_URL', fallback_url) - plexpy.CONFIG.write() + if not server['pms_url']: + server['pms_url'] = fallback_url logger.warn(u"Tautulli PlexTV :: Unable to retrieve server URLs. Using user-defined value without SSL.") - # Not using SSL, remote has no effect + # Not using SSL, remote has no effect else: - if plexpy.CONFIG.PMS_URL_MANUAL and plexpy.CONFIG.PMS_SSL: - fallback_url = fallback_url.replace('http://', 'https://') - - plexpy.CONFIG.__setattr__('PMS_URL', fallback_url) - plexpy.CONFIG.write() + server['pms_url'] = fallback_url logger.info(u"Tautulli PlexTV :: Using user-defined URL.") + plexpy.CONFIG.process_kwargs(server) + plexpy.CONFIG.write() + class PlexTV(object): """ Plex.tv authentication """ - def __init__(self, username='', password='', token=None): - self.protocol = 'HTTPS' + def __init__(self, username=None, password=None, token=None): self.username = username self.password = password + self.token = token + + self.urls = 'https://plex.tv' + self.timeout = plexpy.CONFIG.PMS_TIMEOUT self.ssl_verify = plexpy.CONFIG.VERIFY_SSL_CERT - if not token: + if not self.token: # Check if we should use the admin token, or the guest server token if session.get_session_user_id(): user_data = users.Users() @@ -161,12 +129,14 @@ class PlexTV(object): self.token = user_tokens['server_token'] else: self.token = plexpy.CONFIG.PMS_TOKEN - else: - self.token = token - self.request_handler = http_handler.HTTPHandler(host='plex.tv', - port=443, + if not self.token: + logger.error(u"Tautulli PlexTV :: PlexTV called, but no token provided.") + return + + self.request_handler = http_handler.HTTPHandler(urls=self.urls, token=self.token, + timeout=self.timeout, ssl_verify=self.ssl_verify) def get_plex_auth(self, output_format='raw'): @@ -183,7 +153,6 @@ class PlexTV(object): } request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='POST', headers=headers, output_format=output_format, @@ -265,7 +234,6 @@ class PlexTV(object): def get_plextv_friends(self, output_format=''): uri = '/api/users' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -274,7 +242,6 @@ class PlexTV(object): def get_plextv_user_details(self, output_format=''): uri = '/users/account' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -283,7 +250,6 @@ class PlexTV(object): def get_plextv_devices_list(self, output_format=''): uri = '/devices.xml' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -292,7 +258,6 @@ class PlexTV(object): def get_plextv_server_list(self, output_format=''): uri = '/pms/servers.xml' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -301,7 +266,6 @@ class PlexTV(object): def get_plextv_sync_lists(self, machine_id='', output_format=''): uri = '/servers/%s/sync_lists' % machine_id request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -313,7 +277,6 @@ class PlexTV(object): else: uri = '/api/resources' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -325,7 +288,6 @@ class PlexTV(object): else: uri = '/api/downloads/1.json' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -334,7 +296,6 @@ class PlexTV(object): def delete_plextv_device(self, device_id='', output_format=''): uri = '/devices/%s.xml' % device_id request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='DELETE', output_format=output_format) @@ -343,7 +304,6 @@ class PlexTV(object): def delete_plextv_device_sync_lists(self, client_id='', output_format=''): uri = '/devices/%s/sync_items' % client_id request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -352,197 +312,174 @@ class PlexTV(object): def delete_plextv_sync(self, client_id='', sync_id='', output_format=''): uri = '/devices/%s/sync_items/%s' % (client_id, sync_id) request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='DELETE', output_format=output_format) return request def get_full_users_list(self): - friends_list = self.get_plextv_friends() - own_account = self.get_plextv_user_details() + friends_list = self.get_plextv_friends(output_format='xml') + own_account = self.get_plextv_user_details(output_format='xml') users_list = [] try: - xml_parse = minidom.parseString(own_account) + xml_head = own_account.getElementsByTagName('user') except Exception as e: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list own account: %s" % e) - return [] - except: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list own account.") - return [] + logger.warn(u"Tautulli PlexTV :: Unable to parse own account XML for get_full_users_list: %s." % e) + return {} - xml_head = xml_parse.getElementsByTagName('user') - if not xml_head: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list.") - else: - for a in xml_head: - own_details = {"user_id": helpers.get_xml_attr(a, 'id'), - "username": helpers.get_xml_attr(a, 'username'), - "thumb": helpers.get_xml_attr(a, 'thumb'), - "email": helpers.get_xml_attr(a, 'email'), - "is_home_user": helpers.get_xml_attr(a, 'home'), - "is_allow_sync": None, - "is_restricted": helpers.get_xml_attr(a, 'restricted'), - "filter_all": helpers.get_xml_attr(a, 'filterAll'), - "filter_movies": helpers.get_xml_attr(a, 'filterMovies'), - "filter_tv": helpers.get_xml_attr(a, 'filterTelevision'), - "filter_music": helpers.get_xml_attr(a, 'filterMusic'), - "filter_photos": helpers.get_xml_attr(a, 'filterPhotos') - } + for a in xml_head: + own_details = {"user_id": helpers.get_xml_attr(a, 'id'), + "username": helpers.get_xml_attr(a, 'username'), + "thumb": helpers.get_xml_attr(a, 'thumb'), + "email": helpers.get_xml_attr(a, 'email'), + "is_home_user": helpers.get_xml_attr(a, 'home'), + "is_allow_sync": None, + "is_restricted": helpers.get_xml_attr(a, 'restricted'), + "filter_all": helpers.get_xml_attr(a, 'filterAll'), + "filter_movies": helpers.get_xml_attr(a, 'filterMovies'), + "filter_tv": helpers.get_xml_attr(a, 'filterTelevision'), + "filter_music": helpers.get_xml_attr(a, 'filterMusic'), + "filter_photos": helpers.get_xml_attr(a, 'filterPhotos') + } - users_list.append(own_details) + users_list.append(own_details) try: - xml_parse = minidom.parseString(friends_list) + xml_head = friends_list.getElementsByTagName('User') except Exception as e: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list friends list: %s" % e) - return [] - except: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list friends list.") - return [] + logger.warn(u"Tautulli PlexTV :: Unable to parse friends list XML for get_full_users_list: %s." % e) + return {} - xml_head = xml_parse.getElementsByTagName('User') - if not xml_head: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list.") - else: - for a in xml_head: - friend = {"user_id": helpers.get_xml_attr(a, 'id'), - "username": helpers.get_xml_attr(a, 'title'), - "thumb": helpers.get_xml_attr(a, 'thumb'), - "email": helpers.get_xml_attr(a, 'email'), - "is_home_user": helpers.get_xml_attr(a, 'home'), - "is_allow_sync": helpers.get_xml_attr(a, 'allowSync'), - "is_restricted": helpers.get_xml_attr(a, 'restricted'), - "filter_all": helpers.get_xml_attr(a, 'filterAll'), - "filter_movies": helpers.get_xml_attr(a, 'filterMovies'), - "filter_tv": helpers.get_xml_attr(a, 'filterTelevision'), - "filter_music": helpers.get_xml_attr(a, 'filterMusic'), - "filter_photos": helpers.get_xml_attr(a, 'filterPhotos') - } + for a in xml_head: + friend = {"user_id": helpers.get_xml_attr(a, 'id'), + "username": helpers.get_xml_attr(a, 'title'), + "thumb": helpers.get_xml_attr(a, 'thumb'), + "email": helpers.get_xml_attr(a, 'email'), + "is_home_user": helpers.get_xml_attr(a, 'home'), + "is_allow_sync": helpers.get_xml_attr(a, 'allowSync'), + "is_restricted": helpers.get_xml_attr(a, 'restricted'), + "filter_all": helpers.get_xml_attr(a, 'filterAll'), + "filter_movies": helpers.get_xml_attr(a, 'filterMovies'), + "filter_tv": helpers.get_xml_attr(a, 'filterTelevision'), + "filter_music": helpers.get_xml_attr(a, 'filterMusic'), + "filter_photos": helpers.get_xml_attr(a, 'filterPhotos') + } - users_list.append(friend) + users_list.append(friend) return users_list def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None, rating_key_filter=None): - sync_list = self.get_plextv_sync_lists(machine_id) + sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml') user_data = users.Users() synced_items = [] try: - xml_parse = minidom.parseString(sync_list) + xml_head = sync_list.getElementsByTagName('SyncList') except Exception as e: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items: %s" % e) - return [] - except: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items.") - return [] + logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items: %s." % e) + return {} - xml_head = xml_parse.getElementsByTagName('SyncList') + for a in xml_head: + client_id = helpers.get_xml_attr(a, 'clientIdentifier') - if not xml_head: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items.") - else: - for a in xml_head: - client_id = helpers.get_xml_attr(a, 'clientIdentifier') + # Filter by client_id + if client_id_filter and client_id_filter != client_id: + continue - # Filter by client_id - if client_id_filter and client_id_filter != client_id: - continue + sync_id = helpers.get_xml_attr(a, 'id') + sync_device = a.getElementsByTagName('Device') - sync_id = helpers.get_xml_attr(a, 'id') - sync_device = a.getElementsByTagName('Device') + for device in sync_device: + device_user_id = helpers.get_xml_attr(device, 'userID') + try: + device_username = user_data.get_details(user_id=device_user_id)['username'] + device_friendly_name = user_data.get_details(user_id=device_user_id)['friendly_name'] + except: + device_username = '' + device_friendly_name = '' + device_name = helpers.get_xml_attr(device, 'name') + device_product = helpers.get_xml_attr(device, 'product') + device_product_version = helpers.get_xml_attr(device, 'productVersion') + device_platform = helpers.get_xml_attr(device, 'platform') + device_platform_version = helpers.get_xml_attr(device, 'platformVersion') + device_type = helpers.get_xml_attr(device, 'device') + device_model = helpers.get_xml_attr(device, 'model') + device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt') - for device in sync_device: - device_user_id = helpers.get_xml_attr(device, 'userID') - try: - device_username = user_data.get_details(user_id=device_user_id)['username'] - device_friendly_name = user_data.get_details(user_id=device_user_id)['friendly_name'] - except: - device_username = '' - device_friendly_name = '' - device_name = helpers.get_xml_attr(device, 'name') - device_product = helpers.get_xml_attr(device, 'product') - device_product_version = helpers.get_xml_attr(device, 'productVersion') - device_platform = helpers.get_xml_attr(device, 'platform') - device_platform_version = helpers.get_xml_attr(device, 'platformVersion') - device_type = helpers.get_xml_attr(device, 'device') - device_model = helpers.get_xml_attr(device, 'model') - device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt') + # Filter by user_id + if user_id_filter and user_id_filter != device_user_id: + continue - # Filter by user_id - if user_id_filter and user_id_filter != device_user_id: - continue + for synced in a.getElementsByTagName('SyncItems'): + sync_item = synced.getElementsByTagName('SyncItem') + for item in sync_item: - for synced in a.getElementsByTagName('SyncItems'): - sync_item = synced.getElementsByTagName('SyncItem') - for item in sync_item: + for location in item.getElementsByTagName('Location'): + clean_uri = helpers.get_xml_attr(location, 'uri').split('%2F') - for location in item.getElementsByTagName('Location'): - clean_uri = helpers.get_xml_attr(location, 'uri').split('%2F') + rating_key = next((clean_uri[(idx + 1) % len(clean_uri)] + for idx, item in enumerate(clean_uri) if item == 'metadata'), None) - rating_key = next((clean_uri[(idx + 1) % len(clean_uri)] - for idx, item in enumerate(clean_uri) if item == 'metadata'), None) + # Filter by rating_key + if rating_key_filter and rating_key_filter != rating_key: + continue - # Filter by rating_key - if rating_key_filter and rating_key_filter != rating_key: - continue + sync_id = helpers.get_xml_attr(item, 'id') + sync_version = helpers.get_xml_attr(item, 'version') + sync_root_title = helpers.get_xml_attr(item, 'rootTitle') + sync_title = helpers.get_xml_attr(item, 'title') + sync_metadata_type = helpers.get_xml_attr(item, 'metadataType') + sync_content_type = helpers.get_xml_attr(item, 'contentType') - sync_id = helpers.get_xml_attr(item, 'id') - sync_version = helpers.get_xml_attr(item, 'version') - sync_root_title = helpers.get_xml_attr(item, 'rootTitle') - sync_title = helpers.get_xml_attr(item, 'title') - sync_metadata_type = helpers.get_xml_attr(item, 'metadataType') - sync_content_type = helpers.get_xml_attr(item, 'contentType') + for status in item.getElementsByTagName('Status'): + status_failure_code = helpers.get_xml_attr(status, 'failureCode') + status_failure = helpers.get_xml_attr(status, 'failure') + status_state = helpers.get_xml_attr(status, 'state') + status_item_count = helpers.get_xml_attr(status, 'itemsCount') + status_item_complete_count = helpers.get_xml_attr(status, 'itemsCompleteCount') + status_item_downloaded_count = helpers.get_xml_attr(status, 'itemsDownloadedCount') + status_item_ready_count = helpers.get_xml_attr(status, 'itemsReadyCount') + status_item_successful_count = helpers.get_xml_attr(status, 'itemsSuccessfulCount') + status_total_size = helpers.get_xml_attr(status, 'totalSize') + status_item_download_percent_complete = helpers.get_percent( + status_item_downloaded_count, status_item_count) - for status in item.getElementsByTagName('Status'): - status_failure_code = helpers.get_xml_attr(status, 'failureCode') - status_failure = helpers.get_xml_attr(status, 'failure') - status_state = helpers.get_xml_attr(status, 'state') - status_item_count = helpers.get_xml_attr(status, 'itemsCount') - status_item_complete_count = helpers.get_xml_attr(status, 'itemsCompleteCount') - status_item_downloaded_count = helpers.get_xml_attr(status, 'itemsDownloadedCount') - status_item_ready_count = helpers.get_xml_attr(status, 'itemsReadyCount') - status_item_successful_count = helpers.get_xml_attr(status, 'itemsSuccessfulCount') - status_total_size = helpers.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 = helpers.get_xml_attr(settings, 'audioBoost') + settings_music_bitrate = helpers.get_xml_attr(settings, 'musicBitrate') + settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality') + settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution') + settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality') + settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution') - for settings in item.getElementsByTagName('MediaSettings'): - settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost') - settings_music_bitrate = helpers.get_xml_attr(settings, 'musicBitrate') - settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality') - settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution') - settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality') - settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution') + sync_details = {"device_name": helpers.sanitize(device_name), + "platform": helpers.sanitize(device_platform), + "username": helpers.sanitize(device_username), + "friendly_name": helpers.sanitize(device_friendly_name), + "user_id": device_user_id, + "root_title": helpers.sanitize(sync_root_title), + "title": helpers.sanitize(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, + "client_id": client_id, + "sync_id": sync_id + } - sync_details = {"device_name": helpers.sanitize(device_name), - "platform": helpers.sanitize(device_platform), - "username": helpers.sanitize(device_username), - "friendly_name": helpers.sanitize(device_friendly_name), - "user_id": device_user_id, - "root_title": helpers.sanitize(sync_root_title), - "title": helpers.sanitize(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, - "client_id": client_id, - "sync_id": sync_id - } - - synced_items.append(sync_details) + synced_items.append(sync_details) return session.filter_session_info(synced_items, filter_key='user_id') @@ -550,27 +487,16 @@ class PlexTV(object): logger.info(u"Tautulli PlexTV :: Deleting sync item '%s'." % sync_id) self.delete_plextv_sync(client_id=client_id, sync_id=sync_id) - def get_server_urls(self, include_https=True): + def get_server_connections(self, pms_identifier='', pms_ip='', pms_port=32400, include_https=True): - if plexpy.CONFIG.PMS_IDENTIFIER: - server_id = plexpy.CONFIG.PMS_IDENTIFIER - else: - logger.error(u"Tautulli PlexTV :: Unable to retrieve server identity.") + if not pms_identifier: + logger.error(u"Tautulli PlexTV :: Unable to retrieve server connections: no pms_identifier provided.") return {} - plextv_resources = self.get_plextv_resources(include_https=include_https) - + plextv_resources = self.get_plextv_resources(include_https=include_https, + output_format='xml') try: - xml_parse = minidom.parseString(plextv_resources) - except Exception as e: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls: %s" % e) - return {} - except: - logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls.") - return {} - - try: - xml_head = xml_parse.getElementsByTagName('Device') + xml_head = plextv_resources.getElementsByTagName('Device') except Exception as e: logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls: %s." % e) return {} @@ -580,16 +506,20 @@ class PlexTV(object): conn = [] connections = device.getElementsByTagName('Connection') - server = {"platform": helpers.get_xml_attr(device, 'platform'), - "version": helpers.get_xml_attr(device, 'productVersion') + server = {'pms_identifier': helpers.get_xml_attr(device, 'clientIdentifier'), + 'pms_name': helpers.get_xml_attr(device, 'name'), + 'pms_version': helpers.get_xml_attr(device, 'productVersion'), + 'pms_platform': helpers.get_xml_attr(device, 'platform'), + 'pms_presence': helpers.get_xml_attr(device, 'presence'), + 'pms_is_cloud': 1 if helpers.get_xml_attr(device, 'platform') == 'Cloud' else 0 } for c in connections: - server_details = {"protocol": helpers.get_xml_attr(c, 'protocol'), - "address": helpers.get_xml_attr(c, 'address'), - "port": helpers.get_xml_attr(c, 'port'), - "uri": helpers.get_xml_attr(c, 'uri'), - "local": helpers.get_xml_attr(c, 'local') + server_details = {'protocol': helpers.get_xml_attr(c, 'protocol'), + 'address': helpers.get_xml_attr(c, 'address'), + 'port': helpers.get_xml_attr(c, 'port'), + 'uri': helpers.get_xml_attr(c, 'uri'), + 'local': helpers.get_xml_attr(c, 'local') } conn.append(server_details) @@ -600,10 +530,10 @@ class PlexTV(object): # Try to match the device for a in xml_head: - if helpers.get_xml_attr(a, 'clientIdentifier') == server_id: + if helpers.get_xml_attr(a, 'clientIdentifier') == pms_identifier: server = get_connections(a) break - + # Else no device match found if not server: # Try to match the PMS_IP and PMS_PORT @@ -612,15 +542,8 @@ class PlexTV(object): connections = a.getElementsByTagName('Connection') for connection in connections: - if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \ - int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT: - - plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier') - plexpy.CONFIG.write() - - logger.info(u"Tautulli PlexTV :: PMS identifier changed from %s to %s." - % (server_id, plexpy.CONFIG.PMS_IDENTIFIER)) - + if helpers.get_xml_attr(connection, 'address') == pms_ip and \ + helpers.get_xml_attr(connection, 'port') == str(pms_port): server = get_connections(a) break @@ -649,7 +572,7 @@ class PlexTV(object): return server_times - def discover(self, include_cloud=True): + def discover(self, include_cloud=True, all_servers=False): """ Query plex for all servers online. Returns the ones you own in a selectize format """ servers = self.get_plextv_resources(include_https=True, output_format='xml') clean_servers = [] @@ -679,15 +602,16 @@ class PlexTV(object): connections = d.getElementsByTagName('Connection') for c in connections: - # If this is a remote server don't show any local IPs. - if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \ - helpers.get_xml_attr(c, 'local') == '1': - continue + if not all_servers: + # If this is a remote server don't show any local IPs. + if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \ + helpers.get_xml_attr(c, 'local') == '1': + continue - # If this is a local server don't show any remote IPs. - if helpers.get_xml_attr(d, 'publicAddressMatches') == '1' and \ - helpers.get_xml_attr(c, 'local') == '0': - continue + # If this is a local server don't show any remote IPs. + if helpers.get_xml_attr(d, 'publicAddressMatches') == '1' and \ + helpers.get_xml_attr(c, 'local') == '0': + continue server = {'httpsRequired': helpers.get_xml_attr(d, 'httpsRequired'), 'clientIdentifier': helpers.get_xml_attr(d, 'clientIdentifier'), @@ -770,8 +694,6 @@ class PlexTV(object): return True else: logger.debug(u"Tautulli PlexTV :: Plex Pass subscription not found.") - plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 0) - plexpy.CONFIG.write() return False def get_devices_list(self): diff --git a/plexpy/plexwatch_import.py b/plexpy/plexwatch_import.py index 5ebc64f8..df5fa283 100644 --- a/plexpy/plexwatch_import.py +++ b/plexpy/plexwatch_import.py @@ -22,7 +22,6 @@ import activity_processor import database import helpers import logger -import plextv import users @@ -275,7 +274,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval # Get the latest friends list so we can pull user id's try: - plextv.refresh_users() + users.refresh_users() except: logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.") return None diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py index ff9af316..11e3e2c9 100644 --- a/plexpy/pmsconnect.py +++ b/plexpy/pmsconnect.py @@ -13,16 +13,12 @@ # You should have received a copy of the GNU General Public License # along with Tautulli. If not, see . -import threading import urllib -from urlparse import urlparse import plexpy import common -import database import helpers import http_handler -import libraries import logger import plextv import session @@ -49,83 +45,23 @@ def get_server_friendly_name(): return server_name -def refresh_libraries(): - logger.info(u"Tautulli Pmsconnect :: Requesting libraries list refresh...") - - server_id = plexpy.CONFIG.PMS_IDENTIFIER - if not server_id: - logger.error(u"Tautulli Pmsconnect :: No PMS identifier, cannot refresh libraries. Verify server in settings.") - return - - library_sections = PmsConnect().get_library_details() - - if library_sections: - monitor_db = database.MonitorDatabase() - - library_keys = [] - new_keys = [] - - for section in library_sections: - section_keys = {'server_id': server_id, - 'section_id': section['section_id']} - section_values = {'server_id': server_id, - 'section_id': section['section_id'], - 'section_name': section['section_name'], - 'section_type': section['section_type'], - 'thumb': section['thumb'], - 'art': section['art'], - 'count': section['count'], - 'parent_count': section.get('parent_count', None), - 'child_count': section.get('child_count', None), - } - - result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values) - - library_keys.append(section['section_id']) - - if result == 'insert': - new_keys.append(section['section_id']) - - if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']: - plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys) - plexpy.CONFIG.write() - else: - new_keys = plexpy.CONFIG.HOME_LIBRARY_CARDS + new_keys - plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys) - plexpy.CONFIG.write() - - #if plexpy.CONFIG.UPDATE_SECTION_IDS == 1 or plexpy.CONFIG.UPDATE_SECTION_IDS == -1: - # # Start library section_id update on it's own thread - # threading.Thread(target=libraries.update_section_ids).start() - - #if plexpy.CONFIG.UPDATE_LABELS == 1 or plexpy.CONFIG.UPDATE_LABELS == -1: - # # Start library labels update on it's own thread - # threading.Thread(target=libraries.update_labels).start() - - logger.info(u"Tautulli Pmsconnect :: Libraries list refreshed.") - return True - else: - logger.warn(u"Tautulli Pmsconnect :: Unable to refresh libraries list.") - return False - - class PmsConnect(object): """ Retrieve data from Plex Server """ - def __init__(self, token=None): - if plexpy.CONFIG.PMS_URL: - url_parsed = urlparse(plexpy.CONFIG.PMS_URL) - hostname = url_parsed.hostname - port = url_parsed.port - self.protocol = url_parsed.scheme - else: - hostname = plexpy.CONFIG.PMS_IP - port = plexpy.CONFIG.PMS_PORT - self.protocol = 'http' + def __init__(self, url=None, token=None): + self.url = url + self.token = token - if not token: + if not self.url and plexpy.CONFIG.PMS_URL: + self.url = plexpy.CONFIG.PMS_URL + elif not self.url: + self.url = 'http://{hostname}:{port}'.format(hostname=plexpy.CONFIG.PMS_IP, + port=plexpy.CONFIG.PMS_PORT) + self.timeout = plexpy.CONFIG.PMS_TIMEOUT + + if not self.token: # Check if we should use the admin token, or the guest server token if session.get_session_user_id(): user_data = users.Users() @@ -133,12 +69,10 @@ class PmsConnect(object): self.token = user_tokens['server_token'] else: self.token = plexpy.CONFIG.PMS_TOKEN - else: - self.token = token - self.request_handler = http_handler.HTTPHandler(host=hostname, - port=port, - token=self.token) + self.request_handler = http_handler.HTTPHandler(urls=self.url, + token=self.token, + timeout=self.timeout) def get_sessions(self, output_format=''): """ @@ -150,7 +84,6 @@ class PmsConnect(object): """ uri = '/status/sessions' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -166,7 +99,6 @@ class PmsConnect(object): """ uri = '/status/sessions/terminate?sessionId=%s&reason=%s' % (session_id, reason) request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -183,7 +115,6 @@ class PmsConnect(object): """ uri = '/library/metadata/' + rating_key request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -200,7 +131,6 @@ class PmsConnect(object): """ uri = '/library/metadata/' + rating_key + '/children' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -217,7 +147,6 @@ class PmsConnect(object): """ uri = '/library/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (start, count) request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -234,7 +163,6 @@ class PmsConnect(object): """ uri = '/library/sections/%s/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (section_id, start, count) request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -251,7 +179,6 @@ class PmsConnect(object): """ uri = '/library/metadata/' + rating_key + '/children' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -285,7 +212,6 @@ class PmsConnect(object): """ uri = '/library/metadata/' + rating_key + '/allLeaves' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -301,7 +227,6 @@ class PmsConnect(object): """ uri = '/servers' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -317,7 +242,6 @@ class PmsConnect(object): """ uri = '/:/prefs' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -333,7 +257,6 @@ class PmsConnect(object): """ uri = '/identity' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -349,7 +272,6 @@ class PmsConnect(object): """ uri = '/library/sections' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -368,7 +290,6 @@ class PmsConnect(object): uri = '/library/sections/' + section_id + '/' + list_type + '?X-Plex-Container-Start=0' + count + sort_type + label_key request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -384,7 +305,6 @@ class PmsConnect(object): """ uri = '/library/sections/' + section_id + '/label' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -401,7 +321,6 @@ class PmsConnect(object): """ uri = '/sync/items/' + sync_id request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -417,7 +336,6 @@ class PmsConnect(object): """ uri = '/sync/transcodeQueue' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -433,7 +351,6 @@ class PmsConnect(object): """ uri = '/hubs/search?query=' + urllib.quote(query.encode('utf8')) + '&limit=' + limit + '&includeCollections=1' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -449,7 +366,6 @@ class PmsConnect(object): """ uri = '/myplex/account' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -465,7 +381,6 @@ class PmsConnect(object): """ uri = '/myplex/refreshReachability' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='PUT') return request @@ -480,7 +395,6 @@ class PmsConnect(object): """ uri = '/updater/check?download=0' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='PUT', output_format=output_format) @@ -496,7 +410,6 @@ class PmsConnect(object): """ uri = '/updater/status' request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -515,7 +428,6 @@ class PmsConnect(object): """ uri = '/hubs/home/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s&type=%s' % (start, count, type) request = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', output_format=output_format) @@ -2334,7 +2246,6 @@ class PmsConnect(object): uri = '/photo/:/transcode?%s' % urllib.urlencode(params) result = self.request_handler.make_request(uri=uri, - proto=self.protocol, request_type='GET', return_type=True) diff --git a/plexpy/users.py b/plexpy/users.py index a278f5fa..4c79939b 100644 --- a/plexpy/users.py +++ b/plexpy/users.py @@ -23,9 +23,67 @@ import datatables import helpers import logger import plextv +import pmsconnect import session +def refresh_users(): + logger.info(u"Tautulli Users :: Requesting users list refresh...") + result = plextv.PlexTV().get_full_users_list() + + monitor_db = database.MonitorDatabase() + user_data = Users() + + if result: + for item in result: + + shared_libraries = '' + user_tokens = user_data.get_tokens(user_id=item['user_id']) + if user_tokens and user_tokens['server_token']: + pms_connect = pmsconnect.PmsConnect(token=user_tokens['server_token']) + library_details = pms_connect.get_server_children() + + if library_details: + shared_libraries = ';'.join(d['section_id'] for d in library_details['libraries_list']) + else: + shared_libraries = '' + + control_value_dict = {"user_id": item['user_id']} + new_value_dict = {"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'], + "shared_libraries": shared_libraries, + "filter_all": item['filter_all'], + "filter_movies": item['filter_movies'], + "filter_tv": item['filter_tv'], + "filter_music": item['filter_music'], + "filter_photos": item['filter_photos'] + } + + # Check if we've set a custom avatar if so don't overwrite it. + if item['user_id']: + avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url ' + 'FROM users WHERE user_id = ?', + [item['user_id']]) + if avatar_urls: + if not avatar_urls[0]['custom_avatar_url'] or \ + avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']: + new_value_dict['custom_avatar_url'] = item['thumb'] + else: + new_value_dict['custom_avatar_url'] = item['thumb'] + + monitor_db.upsert('users', new_value_dict, control_value_dict) + + logger.info(u"Tautulli Users :: Users list refreshed.") + return True + else: + logger.warn(u"Tautulli Users :: Unable to refresh users list.") + return False + + class Users(object): def __init__(self): @@ -360,7 +418,7 @@ class Users(object): logger.warn(u"Tautulli Users :: Unable to retrieve user %s from database. Requesting user list refresh." % user_id if user_id else user) # Let's first refresh the user list to make sure the user isn't newly added and not in the db yet - plextv.refresh_users() + refresh_users() user_details = get_user_details(user_id=user_id, user=user) diff --git a/plexpy/web_socket.py b/plexpy/web_socket.py index 5163eba5..0aff6931 100644 --- a/plexpy/web_socket.py +++ b/plexpy/web_socket.py @@ -42,6 +42,7 @@ def start_thread(): def on_disconnect(): activity_processor.ActivityProcessor().set_temp_stopped() + plexpy.initialize_scheduler() def reconnect(): @@ -148,9 +149,8 @@ def run(): logger.info(u"Tautulli WebSocket :: Unable to get an internal response from the server, Plex server is down.") plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'}) plexpy.PLEX_SERVER_UP = False - on_disconnect() - plexpy.initialize_scheduler() + on_disconnect() logger.debug(u"Tautulli WebSocket :: Leaving thread.") diff --git a/plexpy/webauth.py b/plexpy/webauth.py index 350af0c6..3b1377bd 100644 --- a/plexpy/webauth.py +++ b/plexpy/webauth.py @@ -27,9 +27,8 @@ from hashing_passwords import check_hash import plexpy import logger -import plextv from plexpy.database import MonitorDatabase -from plexpy.users import Users +from plexpy.users import Users, refresh_users from plexpy.plextv import PlexTV @@ -72,7 +71,7 @@ def user_login(username=None, password=None): if result: # Refresh the users list to make sure we have all the correct permissions. - plextv.refresh_users() + users.refresh_users() # Successful login return True else: diff --git a/plexpy/webserve.py b/plexpy/webserve.py index c9f970d8..21dddd66 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -16,10 +16,8 @@ import hashlib import json import os -import random import shutil import threading -import uuid import cherrypy from cherrypy.lib.static import serve_file, serve_download @@ -119,7 +117,7 @@ class WebInterface(object): @cherrypy.tools.json_out() @requireAuth(member_of("admin")) @addtoapi("get_server_list") - def discover(self, token=None, include_cloud=True, **kwargs): + def discover(self, token=None, include_cloud=True, all_servers=False, **kwargs): """ Get all your servers that are published to Plex.tv. ``` @@ -150,12 +148,14 @@ class WebInterface(object): plexpy.CONFIG.write() include_cloud = not (include_cloud == 'false') + all_servers = all_servers == 'true' plex_tv = plextv.PlexTV() - servers = plex_tv.discover(include_cloud=include_cloud) + servers_list = plex_tv.discover(include_cloud=include_cloud, + all_servers=all_servers) - if servers: - return servers + if servers_list: + return servers_list ##### Home ##### @@ -170,6 +170,7 @@ class WebInterface(object): "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, "home_stats_recently_added_count": plexpy.CONFIG.HOME_STATS_RECENTLY_ADDED_COUNT, "pms_name": plexpy.CONFIG.PMS_NAME, + "pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD, "update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG } return serve_template(templatename="index.html", title="Home", config=config) @@ -452,7 +453,7 @@ class WebInterface(object): @requireAuth(member_of("admin")) def refresh_libraries_list(self, **kwargs): """ Refresh the libraries list on it's own thread. """ - threading.Thread(target=pmsconnect.refresh_libraries).start() + threading.Thread(target=libraries.refresh_libraries).start() logger.info(u"Manual libraries list refresh requested.") return True @@ -1074,7 +1075,7 @@ class WebInterface(object): @requireAuth(member_of("admin")) def refresh_users_list(self, **kwargs): """ Refresh the users list on it's own thread. """ - threading.Thread(target=plextv.refresh_users).start() + threading.Thread(target=users.refresh_users).start() logger.info(u"Manual users list refresh requested.") return True @@ -2559,6 +2560,8 @@ class WebInterface(object): "pms_port": plexpy.CONFIG.PMS_PORT, "pms_token": plexpy.CONFIG.PMS_TOKEN, "pms_ssl": checked(plexpy.CONFIG.PMS_SSL), + "pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE), + "pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD, "pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL), "pms_uuid": plexpy.CONFIG.PMS_UUID, "pms_web_url": plexpy.CONFIG.PMS_WEB_URL, @@ -2576,7 +2579,6 @@ class WebInterface(object): "refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL, "refresh_users_on_startup": checked(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP), "logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL, - "pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE), "notify_consecutive": checked(plexpy.CONFIG.NOTIFY_CONSECUTIVE), "notify_upload_posters": checked(plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS), "notify_recently_added_upgrade": checked(plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE), @@ -2733,8 +2735,7 @@ class WebInterface(object): # Get new server URLs for SSL communications and get new server friendly name if server_changed: - plextv.get_real_pms_url() - pmsconnect.get_server_friendly_name() + plextv.get_server_resources() web_socket.reconnect() # If first run, start websocket @@ -2751,11 +2752,11 @@ class WebInterface(object): # Refresh users table if our server IP changes. if refresh_libraries: - threading.Thread(target=pmsconnect.refresh_libraries).start() + threading.Thread(target=libraries.refresh_libraries).start() # Refresh users table if our server IP changes. if refresh_users: - threading.Thread(target=plextv.refresh_users).start() + threading.Thread(target=users.refresh_users).start() return {'result': 'success', 'message': 'Settings saved.'} @@ -3425,16 +3426,15 @@ class WebInterface(object): # Fallback to checking /identity endpoint is server is unpublished # Cannot set SSL settings on the PMS if unpublished so 'http' is okay if not identifier: - request_handler = http_handler.HTTPHandler(host=hostname, - port=port, - token=None) + scheme = 'https' if ssl else 'http' + url = '{scheme}://{hostname}:{port}'.format(scheme=scheme, hostname=hostname, port=port) uri = '/identity' + + request_handler = http_handler.HTTPHandler(urls=url, + ssl_verify=False) request = request_handler.make_request(uri=uri, - proto='http', request_type='GET', - output_format='xml', - no_token=True, - timeout=10) + output_format='xml') if request: xml_head = request.getElementsByTagName('MediaContainer')[0] identifier = xml_head.getAttribute('machineIdentifier')