class Notifier(object): def __init__(self): self.session = MedusaSession() self.session.headers.update({ 'X-Plex-Device-Name': 'Medusa', 'X-Plex-Product': 'Medusa Notifier', 'X-Plex-Client-Identifier': common.USER_AGENT, 'X-Plex-Version': app.APP_VERSION, }) @staticmethod def _notify_pht(title, message, host=None, username=None, password=None, force=False): # pylint: disable=too-many-arguments """Internal wrapper for the notify_snatch and notify_download functions Args: message: Message body of the notice to send title: Title of the notice to send host: Plex Home Theater(s) host:port username: Plex username password: Plex password force: Used for the Test method to override config safety checks Returns: Returns a list results in the format of host:ip:result The result will either be 'OK' or False, this is used to be parsed by the calling function. """ from medusa.notifiers import kodi_notifier # suppress notifications if the notifier is disabled but the notify options are checked if not app.USE_PLEX_CLIENT and not force: return False host = host or app.PLEX_CLIENT_HOST username = username or app.PLEX_CLIENT_USERNAME password = password or app.PLEX_CLIENT_PASSWORD return kodi_notifier._notify_kodi(title, message, host=host, username=username, password=password, force=force, dest_app='PLEX') # pylint: disable=protected-access ############################################################################## # Public functions ############################################################################## def notify_snatch(self, title, message, **kwargs): if app.PLEX_NOTIFY_ONSNATCH: self._notify_pht(title, message) def notify_download(self, ep_obj): if app.PLEX_NOTIFY_ONDOWNLOAD: self._notify_pht(common.notifyStrings[common.NOTIFY_DOWNLOAD], ep_obj.pretty_name_with_quality()) def notify_subtitle_download(self, ep_obj, lang): if app.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD: self._notify_pht( common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD], ep_obj.pretty_name() + ': ' + lang) def notify_git_update(self, new_version='??'): if app.NOTIFY_ON_UPDATE: update_text = common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] title = common.notifyStrings[common.NOTIFY_GIT_UPDATE] if update_text and title and new_version: self._notify_pht(title, update_text + new_version) def notify_login(self, ipaddress=''): if app.NOTIFY_ON_LOGIN: update_text = common.notifyStrings[common.NOTIFY_LOGIN_TEXT] title = common.notifyStrings[common.NOTIFY_LOGIN] if update_text and title and ipaddress: self._notify_pht(title, update_text.format(ipaddress)) def test_notify_pht(self, host, username, password): return self._notify_pht('Test Notification', 'This is a test notification from Medusa', host, username, password, force=True) def test_notify_pms(self, host, username, password, plex_server_token): return self.update_library(hosts=host, username=username, password=password, plex_server_token=plex_server_token, force=True) def update_library( self, ep_obj=None, hosts=None, # pylint: disable=too-many-arguments, too-many-locals, too-many-statements, too-many-branches username=None, password=None, plex_server_token=None, force=False): """Handles updating the Plex Media Server host via HTTP API Plex Media Server currently only supports updating the whole video library and not a specific path. Returns: Returns None for no issue, else a string of host with connection issues """ if not (app.USE_PLEX_SERVER and app.PLEX_UPDATE_LIBRARY) and not force: return None hosts = hosts or app.PLEX_SERVER_HOST if not hosts: log.debug( u'PLEX: No Plex Media Server host specified, check your settings' ) return False if not self.get_token(username, password, plex_server_token): log.warning( u'PLEX: Error getting auth token for Plex Media Server, check your settings' ) return False file_location = '' if not ep_obj else ep_obj.location gen_hosts = generate(hosts) hosts = (x.strip() for x in gen_hosts if x.strip()) all_hosts = {} matching_hosts = {} failed_hosts = set() schema = 'https' if app.PLEX_SERVER_HTTPS else 'http' for cur_host in hosts: url = '{schema}://{host}/library/sections'.format(schema=schema, host=cur_host) try: response = self.session.get(url) except requests.RequestException as error: log.warning( u'PLEX: Error while trying to contact Plex Media Server: {0}', ex(error)) failed_hosts.add(cur_host) continue try: response.raise_for_status() except requests.RequestException as error: if response.status_code == 401: log.warning( u'PLEX: Unauthorized. Please set TOKEN or USERNAME and PASSWORD in Plex settings' ) else: log.warning( u'PLEX: Error while trying to contact Plex Media Server: {0}', ex(error)) failed_hosts.add(cur_host) continue else: xml_response = response.text if not xml_response: log.warning( u'PLEX: Error while trying to contact Plex Media Server: {0}', cur_host) failed_hosts.add(cur_host) continue else: media_container = etree.fromstring(xml_response) sections = media_container.findall('.//Directory') if not sections: log.debug(u'PLEX: Plex Media Server not running on: {0}', cur_host) failed_hosts.add(cur_host) continue for section in sections: if 'show' == section.attrib['type']: key = str(section.attrib['key']) keyed_host = { key: cur_host, } all_hosts.update(keyed_host) if not file_location: continue for section_location in section.findall('.//Location'): section_path = re.sub( r'[/\\]+', '/', section_location.attrib['path'].lower()) section_path = re.sub(r'^(.{,2})[/\\]', '', section_path) location_path = re.sub(r'[/\\]+', '/', file_location.lower()) location_path = re.sub(r'^(.{,2})[/\\]', '', location_path) if section_path in location_path: matching_hosts.update(keyed_host) if force: return ', '.join(failed_hosts) if failed_hosts else None if matching_hosts: hosts_try = matching_hosts result = u'PLEX: Updating hosts where TV section paths match the downloaded show: {0}' else: hosts_try = all_hosts result = u'PLEX: Updating all hosts with TV sections: {0}' log.debug(result.format(', '.join(hosts_try))) for section_key, cur_host in iteritems(hosts_try): url = '{schema}://{host}/library/sections/{key}/refresh'.format( schema=schema, host=cur_host, key=section_key, ) try: response = self.session.get(url) except requests.RequestException as error: log.warning( u'PLEX: Error updating library section for Plex Media Server: {0}', ex(error)) failed_hosts.add(cur_host) else: del response # request succeeded so response is not needed return ', '.join(failed_hosts) if failed_hosts else None def get_token(self, username=None, password=None, plex_server_token=None): """ Get auth token. Try to get the auth token from the argument, the config, the session, or the Plex website in that order. :param username: plex.tv username :param password: plex.tv password :param plex_server_token: auth token :returns: Plex auth token being used or True if authentication is not required, else None """ username = username or app.PLEX_SERVER_USERNAME password = password or app.PLEX_SERVER_PASSWORD plex_server_token = plex_server_token or app.PLEX_SERVER_TOKEN if plex_server_token: self.session.headers['X-Plex-Token'] = plex_server_token if 'X-Plex-Token' in self.session.headers: return self.session.headers['X-Plex-Token'] if not (username and password): return True log.debug(u'PLEX: fetching plex.tv credentials for user: {0}', username) error_msg = u'PLEX: Error fetching credentials from plex.tv for user {0}: {1}' try: # sign in response = self.session.post('https://plex.tv/users/sign_in.json', data={ 'user[login]': username, 'user[password]': password, }) response.raise_for_status() except requests.RequestException as error: log.debug(error_msg, username, error) return try: # get json data data = response.json() except ValueError as error: log.debug(error_msg, username, error) return try: # get token from key plex_server_token = data['user']['authentication_token'] except KeyError as error: log.debug(error_msg, username, error) return else: self.session.headers['X-Plex-Token'] = plex_server_token return self.session.headers.get('X-Plex-Token')
class Notifier(object): def __init__(self): self.session = MedusaSession() self.url = 'https://api.pushbullet.com/v2/' def test_notify(self, pushbullet_api): log.debug('Sending a test Pushbullet notification.') return self._sendPushbullet( pushbullet_api, event='Test', message='Testing Pushbullet settings from Medusa', force=True) def get_devices(self, pushbullet_api): log.debug( 'Testing Pushbullet authentication and retrieving the device list.' ) headers = { 'Access-Token': pushbullet_api, 'Content-Type': 'application/json' } try: r = self.session.get(urljoin(self.url, 'devices'), headers=headers) return r.text except ValueError: return {} def notify_snatch(self, title, message, **kwargs): if app.PUSHBULLET_NOTIFY_ONSNATCH: self._sendPushbullet(pushbullet_api=None, event=title, message=message) def notify_download(self, ep_obj): if app.PUSHBULLET_NOTIFY_ONDOWNLOAD: self._sendPushbullet( pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_DOWNLOAD] + ': ' + ep_obj.pretty_name_with_quality(), message=ep_obj.pretty_name_with_quality()) def notify_subtitle_download(self, ep_obj, lang): if app.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD: self._sendPushbullet( pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD] + ': ' + ep_obj.pretty_name() + ': ' + lang, message=ep_obj.pretty_name() + ': ' + lang) def notify_git_update(self, new_version='??'): link = re.match(r'.*href="(.*?)" .*', app.NEWEST_VERSION_STRING) if link: link = link.group(1) self._sendPushbullet( pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_GIT_UPDATE], message=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] + new_version, link=link) def notify_login(self, ipaddress=''): self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_LOGIN], message=common.notifyStrings[ common.NOTIFY_LOGIN_TEXT].format(ipaddress)) def _sendPushbullet( # pylint: disable=too-many-arguments self, pushbullet_api=None, pushbullet_device=None, event=None, message=None, link=None, force=False): push_result = {'success': False, 'error': ''} if not (app.USE_PUSHBULLET or force): return False pushbullet_api = pushbullet_api or app.PUSHBULLET_API pushbullet_device = pushbullet_device or app.PUSHBULLET_DEVICE log.debug('Pushbullet event: {0!r}', event) log.debug('Pushbullet message: {0!r}', message) log.debug('Pushbullet api: {0!r}', pushbullet_api) log.debug('Pushbullet devices: {0!r}', pushbullet_device) post_data = { 'title': event, 'body': message, 'device_iden': pushbullet_device, 'type': 'link' if link else 'note' } if link: post_data['url'] = link headers = { 'Access-Token': pushbullet_api, 'Content-Type': 'application/json' } r = self.session.post(urljoin(self.url, 'pushes'), json=post_data, headers=headers) try: response = r.json() except ValueError: log.warning( 'Pushbullet notification failed. Could not parse pushbullet response.' ) push_result[ 'error'] = 'Pushbullet notification failed. Could not parse pushbullet response.' return push_result failed = response.pop('error', {}) if failed: log.warning('Pushbullet notification failed: {0}', failed.get('message')) push_result[ 'error'] = 'Pushbullet notification failed: {0}'.format( failed.get('message')) else: log.debug('Pushbullet notification sent.') push_result['success'] = True return push_result
class GenericClient(object): """Base class for all torrent clients.""" def __init__(self, name, host=None, username=None, password=None): """Constructor. :param name: :type name: string :param host: :type host: string :param username: :type username: string :param password: :type password: string """ self.name = name self.username = app.TORRENT_USERNAME if username is None else username self.password = app.TORRENT_PASSWORD if password is None else password self.host = app.TORRENT_HOST if host is None else host self.rpcurl = app.TORRENT_RPCURL self.url = None self.response = None self.auth = None self.last_time = time.time() self.session = MedusaSession() self.session.auth = (self.username, self.password) def _request(self, method='get', params=None, data=None, files=None, cookies=None): if time.time() > self.last_time + 1800 or not self.auth: self.last_time = time.time() self._get_auth() text_params = str(params) text_data = str(data) text_files = str(files) log.debug( '{name}: Requested a {method} connection to {url} with' ' params: {params} Data: {data} Files: {files}', { 'name': self.name, 'method': method.upper(), 'url': self.url, 'params': text_params[0:99] + '...' if len(text_params) > 102 else text_params, 'data': text_data[0:99] + '...' if len(text_data) > 102 else text_data, 'files': text_files[0:99] + '...' if len(text_files) > 102 else text_files, } ) if not self.auth: log.warning('{name}: Authentication Failed', {'name': self.name}) return False try: self.response = self.session.request(method, self.url, params=params, data=data, files=files, cookies=cookies, timeout=120, verify=False) except (requests.exceptions.MissingSchema, requests.exceptions.InvalidURL) as error: log.warning('{name}: Invalid Host: {error}', {'name': self.name, 'error': error}) return False except requests.exceptions.RequestException as error: log.warning('{name}: Error occurred during request: {error}', {'name': self.name, 'error': error}) return False except Exception as error: log.error('{name}: Unknown exception raised when sending torrent to' ' {name}: {error}', {'name': self.name, 'error': error}) return False if self.response.status_code == 401: log.error('{name}: Invalid Username or Password,' ' check your config', {'name': self.name}) return False code_description = http_code_description(self.response.status_code) if code_description is not None: log.info('{name}: {code}', {'name': self.name, 'code': code_description}) return False log.debug('{name}: Response to {method} request is {response}', { 'name': self.name, 'method': method.upper(), 'response': self.response.text[0:1024] + '...' if len(self.response.text) > 1027 else self.response.text }) return True def _get_auth(self): """Return the auth_id needed for the client.""" raise NotImplementedError def _add_torrent_uri(self, result): """Return the True/False from the client when a torrent is added via url (magnet or .torrent link). :param result: :type result: medusa.classes.SearchResult """ raise NotImplementedError def _add_torrent_file(self, result): """Return the True/False from the client when a torrent is added via result.content (only .torrent file). :param result: :type result: medusa.classes.SearchResult """ raise NotImplementedError def _set_torrent_label(self, result): """Return the True/False from the client when a torrent is set with label. :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True def _set_torrent_ratio(self, result): """Return the True/False from the client when a torrent is set with ratio. :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True def _set_torrent_seed_time(self, result): """Return the True/False from the client when a torrent is set with a seed time. :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True def _set_torrent_priority(self, result): """Return the True/False from the client when a torrent is set with result.priority (-1 = low, 0 = normal, 1 = high). :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True def _set_torrent_path(self, torrent_path): """Return the True/False from the client when a torrent is set with path. :param torrent_path: :type torrent_path: string :return: :rtype: bool """ return True def _set_torrent_pause(self, result): """Return the True/False from the client when a torrent is set with pause. :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True @staticmethod def _get_info_hash(result): if result.url.startswith('magnet:'): result.hash = re.findall(r'urn:btih:([\w]{32,40})', result.url)[0] if len(result.hash) == 32: result.hash = b16encode(b32decode(result.hash)).lower() else: try: # `bencode.bdecode` is monkeypatched in `medusa.init` torrent_bdecode = bdecode(result.content, allow_extra_data=True) info = torrent_bdecode['info'] result.hash = sha1(bencode(info)).hexdigest() except (BencodeDecodeError, KeyError): log.warning( 'Unable to bdecode torrent. Invalid torrent: {name}. ' 'Deleting cached result if exists', {'name': result.name} ) cache_db_con = db.DBConnection('cache.db') cache_db_con.action( 'DELETE FROM [{provider}] ' 'WHERE name = ? '.format(provider=result.provider.get_id()), [result.name] ) except Exception: log.error(traceback.format_exc()) return result def send_torrent(self, result): """Add torrent to the client. :param result: :type result: medusa.classes.SearchResult :return: :rtype: str or bool """ r_code = False log.debug('Calling {name} Client', {'name': self.name}) if not self.auth: if not self._get_auth(): log.warning('{name}: Authentication Failed', {'name': self.name}) return r_code try: # Sets per provider seed ratio result.ratio = result.provider.seed_ratio() # lazy fix for now, I'm sure we already do this somewhere else too result = self._get_info_hash(result) if not result.hash: return False if result.url.startswith('magnet:'): r_code = self._add_torrent_uri(result) else: r_code = self._add_torrent_file(result) if not r_code: log.warning('{name}: Unable to send Torrent', {'name': self.name}) return False if not self._set_torrent_pause(result): log.error('{name}: Unable to set the pause for Torrent', {'name': self.name}) if not self._set_torrent_label(result): log.error('{name}: Unable to set the label for Torrent', {'name': self.name}) if not self._set_torrent_ratio(result): log.error('{name}: Unable to set the ratio for Torrent', {'name': self.name}) if not self._set_torrent_seed_time(result): log.error('{name}: Unable to set the seed time for Torrent', {'name': self.name}) if not self._set_torrent_path(result): log.error('{name}: Unable to set the path for Torrent', {'name': self.name}) if result.priority != 0 and not self._set_torrent_priority(result): log.error('{name}: Unable to set priority for Torrent', {'name': self.name}) except Exception as msg: log.error('{name}: Failed Sending Torrent', {'name': self.name}) log.debug('{name}: Exception raised when sending torrent {result}.' ' Error: {error}', {'name': self.name, 'result': result, 'error': msg}) return r_code return r_code def test_authentication(self): """Test authentication. :return: :rtype: tuple(bool, str) """ try: self.response = self.session.get(self.url, timeout=120, verify=False) except requests.exceptions.ConnectionError: return False, 'Error: {name} Connection Error'.format(name=self.name) except (requests.exceptions.MissingSchema, requests.exceptions.InvalidURL): return False, 'Error: Invalid {name} host'.format(name=self.name) if self.response.status_code == 401: return False, 'Error: Invalid {name} Username or Password, check your config!'.format(name=self.name) try: self._get_auth() if self.response.status_code == 200 and self.auth: return True, 'Success: Connected and Authenticated' else: return False, 'Error: Unable to get {name} Authentication, check your config!'.format(name=self.name) except Exception as error: return False, 'Unable to connect to {name}. Error: {msg}'.format(name=self.name, msg=error) def remove_torrent(self, info_hash): """Remove torrent from client using given info_hash. :param info_hash: :type info_hash: string :return :rtype: bool """ raise NotImplementedError def remove_ratio_reached(self): """Remove all Medusa torrents that ratio was reached. It loops in all hashes returned from client and check if it is in the snatch history if its then it checks if we already processed media from the torrent (episode status `Downloaded`) If is a RARed torrent then we don't have a media file so we check if that hash is from an episode that has a `Downloaded` status """ raise NotImplementedError
class Notifier(object): def __init__(self): self.session = MedusaSession() self.url = 'https://api.pushbullet.com/v2/' def test_notify(self, pushbullet_api): log.debug('Sending a test Pushbullet notification.') return self._sendPushbullet( pushbullet_api, event='Test', message='Testing Pushbullet settings from Medusa', force=True ) def get_devices(self, pushbullet_api): log.debug('Testing Pushbullet authentication and retrieving the device list.') headers = {'Access-Token': pushbullet_api, 'Content-Type': 'application/json'} try: r = self.session.get(urljoin(self.url, 'devices'), headers=headers) return r.text except ValueError: return {} def notify_snatch(self, title, message): if app.PUSHBULLET_NOTIFY_ONSNATCH: self._sendPushbullet( pushbullet_api=None, event=title, message=message ) def notify_download(self, ep_obj): if app.PUSHBULLET_NOTIFY_ONDOWNLOAD: self._sendPushbullet( pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_DOWNLOAD] + ': ' + ep_obj.pretty_name_with_quality(), message=ep_obj.pretty_name_with_quality() ) def notify_subtitle_download(self, ep_obj, lang): if app.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD: self._sendPushbullet( pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD] + ': ' + ep_obj.pretty_name() + ': ' + lang, message=ep_obj.pretty_name() + ': ' + lang ) def notify_git_update(self, new_version='??'): link = re.match(r'.*href="(.*?)" .*', app.NEWEST_VERSION_STRING) if link: link = link.group(1) self._sendPushbullet( pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_GIT_UPDATE], message=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] + new_version, link=link ) def notify_login(self, ipaddress=''): self._sendPushbullet( pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_LOGIN], message=common.notifyStrings[common.NOTIFY_LOGIN_TEXT].format(ipaddress) ) def _sendPushbullet( # pylint: disable=too-many-arguments self, pushbullet_api=None, pushbullet_device=None, event=None, message=None, link=None, force=False): push_result = {'success': False, 'error': ''} if not (app.USE_PUSHBULLET or force): return False pushbullet_api = pushbullet_api or app.PUSHBULLET_API pushbullet_device = pushbullet_device or app.PUSHBULLET_DEVICE log.debug('Pushbullet event: {0!r}', event) log.debug('Pushbullet message: {0!r}', message) log.debug('Pushbullet api: {0!r}', pushbullet_api) log.debug('Pushbullet devices: {0!r}', pushbullet_device) post_data = { 'title': event, 'body': message, 'device_iden': pushbullet_device, 'type': 'link' if link else 'note' } if link: post_data['url'] = link headers = {'Access-Token': pushbullet_api, 'Content-Type': 'application/json'} r = self.session.post(urljoin(self.url, 'pushes'), json=post_data, headers=headers) try: response = r.json() except ValueError: log.warning('Pushbullet notification failed. Could not parse pushbullet response.') push_result['error'] = 'Pushbullet notification failed. Could not parse pushbullet response.' return push_result failed = response.pop('error', {}) if failed: log.warning('Pushbullet notification failed: {0}', failed.get('message')) push_result['error'] = 'Pushbullet notification failed: {0}'.format(failed.get('message')) else: log.debug('Pushbullet notification sent.') push_result['success'] = True return push_result
class GenericClient(object): """Base class for all torrent clients.""" def __init__(self, name, host=None, username=None, password=None): """Constructor. :param name: :type name: string :param host: :type host: string :param username: :type username: string :param password: :type password: string """ self.name = name self.username = app.TORRENT_USERNAME if username is None else username self.password = app.TORRENT_PASSWORD if password is None else password self.host = app.TORRENT_HOST if host is None else host self.rpcurl = app.TORRENT_RPCURL self.url = None self.response = None self.auth = None self.last_time = time.time() self.session = MedusaSession() self.session.auth = (self.username, self.password) def _request(self, method='get', params=None, data=None, files=None, cookies=None): if time.time() > self.last_time + 1800 or not self.auth: self.last_time = time.time() self._get_auth() text_params = str(params) text_data = str(data) log.debug( '{name}: Requested a {method} connection to {url} with' ' params: {params} Data: {data}', { 'name': self.name, 'method': method.upper(), 'url': self.url, 'params': text_params[0:99] + '...' if len(text_params) > 102 else text_params, 'data': text_data[0:99] + '...' if len(text_data) > 102 else text_data }) if not self.auth: log.warning('{name}: Authentication Failed', {'name': self.name}) return False try: self.response = self.session.request(method, self.url, params=params, data=data, files=files, cookies=cookies, timeout=120, verify=False) except requests.exceptions.ConnectionError as msg: log.error('{name}: Unable to connect {error}', { 'name': self.name, 'error': msg }) return False except (requests.exceptions.MissingSchema, requests.exceptions.InvalidURL): log.error('{name}: Invalid Host', {'name': self.name}) return False except requests.exceptions.HTTPError as msg: log.error('{name}: Invalid HTTP Request {error}', { 'name': self.name, 'error': msg }) return False except requests.exceptions.Timeout as msg: log.warning('{name}: Connection Timeout {error}', { 'name': self.name, 'error': msg }) return False except Exception as msg: log.error( '{name}: Unknown exception raised when send torrent to' ' {name} : {error}', { 'name': self.name, 'error': msg }) return False if self.response.status_code == 401: log.error( '{name}: Invalid Username or Password,' ' check your config', {'name': self.name}) return False code_description = http_code_description(self.response.status_code) if code_description is not None: log.info('{name}: {code}', { 'name': self.name, 'code': code_description }) return False log.debug( '{name}: Response to {method} request is {response}', { 'name': self.name, 'method': method.upper(), 'response': self.response.text[0:1024] + '...' if len(self.response.text) > 1027 else self.response.text }) return True def _get_auth(self): """Return the auth_id needed for the client.""" raise NotImplementedError def _add_torrent_uri(self, result): """Return the True/False from the client when a torrent is added via url (magnet or .torrent link). :param result: :type result: medusa.classes.SearchResult """ raise NotImplementedError def _add_torrent_file(self, result): """Return the True/False from the client when a torrent is added via result.content (only .torrent file). :param result: :type result: medusa.classes.SearchResult """ raise NotImplementedError def _set_torrent_label(self, result): """Return the True/False from the client when a torrent is set with label. :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True def _set_torrent_ratio(self, result): """Return the True/False from the client when a torrent is set with ratio. :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True def _set_torrent_seed_time(self, result): """Return the True/False from the client when a torrent is set with a seed time. :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True def _set_torrent_priority(self, result): """Return the True/False from the client when a torrent is set with result.priority (-1 = low, 0 = normal, 1 = high). :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True def _set_torrent_path(self, torrent_path): """Return the True/False from the client when a torrent is set with path. :param torrent_path: :type torrent_path: string :return: :rtype: bool """ return True def _set_torrent_pause(self, result): """Return the True/False from the client when a torrent is set with pause. :param result: :type result: medusa.classes.SearchResult :return: :rtype: bool """ return True @staticmethod def _get_info_hash(result): if result.url.startswith('magnet:'): result.hash = re.findall(r'urn:btih:([\w]{32,40})', result.url)[0] if len(result.hash) == 32: result.hash = b16encode(b32decode(result.hash)).lower() else: try: torrent_bdecode = bdecode(result.content) info = torrent_bdecode['info'] result.hash = sha1(bencode(info)).hexdigest() except (BTFailure, KeyError): log.warning( 'Unable to bdecode torrent. Invalid torrent: {name}. ' 'Deleting cached result if exists', {'name': result.name}) cache_db_con = db.DBConnection('cache.db') cache_db_con.action( b'DELETE FROM [{provider}] ' b'WHERE name = ? '.format( provider=result.provider.get_id()), [result.name]) except Exception: log.error(traceback.format_exc()) return result def send_torrent(self, result): """Add torrent to the client. :param result: :type result: medusa.classes.SearchResult :return: :rtype: str or bool """ r_code = False log.debug('Calling {name} Client', {'name': self.name}) if not self.auth: if not self._get_auth(): log.warning('{name}: Authentication Failed', {'name': self.name}) return r_code try: # Sets per provider seed ratio result.ratio = result.provider.seed_ratio() # lazy fix for now, I'm sure we already do this somewhere else too result = self._get_info_hash(result) if not result.hash: return False if result.url.startswith('magnet:'): r_code = self._add_torrent_uri(result) else: r_code = self._add_torrent_file(result) if not r_code: log.warning('{name}: Unable to send Torrent', {'name': self.name}) return False if not self._set_torrent_pause(result): log.error('{name}: Unable to set the pause for Torrent', {'name': self.name}) if not self._set_torrent_label(result): log.error('{name}: Unable to set the label for Torrent', {'name': self.name}) if not self._set_torrent_ratio(result): log.error('{name}: Unable to set the ratio for Torrent', {'name': self.name}) if not self._set_torrent_seed_time(result): log.error('{name}: Unable to set the seed time for Torrent', {'name': self.name}) if not self._set_torrent_path(result): log.error('{name}: Unable to set the path for Torrent', {'name': self.name}) if result.priority != 0 and not self._set_torrent_priority(result): log.error('{name}: Unable to set priority for Torrent', {'name': self.name}) except Exception as msg: log.error('{name}: Failed Sending Torrent', {'name': self.name}) log.debug( '{name}: Exception raised when sending torrent {result}.' ' Error: {error}', { 'name': self.name, 'result': result, 'error': msg }) return r_code return r_code def test_authentication(self): """Test authentication. :return: :rtype: tuple(bool, str) """ try: self.response = self.session.get(self.url, timeout=120, verify=False) except requests.exceptions.ConnectionError: return False, 'Error: {name} Connection Error'.format( name=self.name) except (requests.exceptions.MissingSchema, requests.exceptions.InvalidURL): return False, 'Error: Invalid {name} host'.format(name=self.name) if self.response.status_code == 401: return False, 'Error: Invalid {name} Username or Password, check your config!'.format( name=self.name) try: self._get_auth() if self.response.status_code == 200 and self.auth: return True, 'Success: Connected and Authenticated' else: return False, 'Error: Unable to get {name} Authentication, check your config!'.format( name=self.name) except Exception as error: return False, 'Unable to connect to {name}. Error: {msg}'.format( name=self.name, msg=error) def remove_torrent(self, info_hash): """Remove torrent from client using given info_hash. :param info_hash: :type info_hash: string :return :rtype: bool """ raise NotImplementedError def remove_ratio_reached(self): """Remove all Medusa torrents that ratio was reached. It loops in all hashes returned from client and check if it is in the snatch history if its then it checks if we already processed media from the torrent (episode status `Downloaded`) If is a RARed torrent then we don't have a media file so we check if that hash is from an episode that has a `Downloaded` status """ raise NotImplementedError
class CheckVersion(object): """Version check class meant to run as a thread object with the sr scheduler.""" def __init__(self): self.updater = None self.install_type = None self.amActive = False self.install_type = self.find_install_type() if self.install_type == 'git': self.updater = GitUpdateManager() elif self.install_type == 'source': self.updater = SourceUpdateManager() self.session = MedusaSession() def run(self, force=False): self.amActive = True # Update remote branches and store in app.GIT_REMOTE_BRANCHES self.list_remote_branches() if self.updater: # set current branch version app.BRANCH = self.get_branch() if self.check_for_new_version(force): if app.AUTO_UPDATE: log.info(u'New update found, starting auto-updater ...') ui.notifications.message( 'New update found, starting auto-updater') if self.run_backup_if_safe(): if app.version_check_scheduler.action.update(): log.info(u'Update was successful!') ui.notifications.message('Update was successful') app.events.put(app.events.SystemEvent.RESTART) else: log.info(u'Update failed!') ui.notifications.message('Update failed!') self.check_for_new_news(force) self.amActive = False def run_backup_if_safe(self): return self.safe_to_update() is True and self._runbackup() is True def _runbackup(self): # Do a system backup before update log.info(u'Config backup in progress...') ui.notifications.message('Backup', 'Config backup in progress...') try: backupDir = os.path.join(app.DATA_DIR, app.BACKUP_DIR) if not os.path.isdir(backupDir): os.mkdir(backupDir) if self._keeplatestbackup(backupDir) and self._backup(backupDir): log.info(u'Config backup successful, updating...') ui.notifications.message( 'Backup', 'Config backup successful, updating...') return True else: log.warning(u'Config backup failed, aborting update') ui.notifications.message( 'Backup', 'Config backup failed, aborting update') return False except Exception as e: log.error(u'Update: Config backup failed. Error: {0!r}', e) ui.notifications.message('Backup', 'Config backup failed, aborting update') return False @staticmethod def _keeplatestbackup(backupDir=None): if not backupDir: return False import glob files = glob.glob(os.path.join(backupDir, '*.zip')) if not files: return True now = time.time() newest = files[0], now - os.path.getctime(files[0]) for f in files[1:]: age = now - os.path.getctime(f) if age < newest[1]: newest = f, age files.remove(newest[0]) for f in files: os.remove(f) return True # TODO: Merge with backup in helpers @staticmethod def _backup(backupDir=None): if not backupDir: return False source = [ os.path.join(app.DATA_DIR, app.APPLICATION_DB), app.CONFIG_FILE, os.path.join(app.DATA_DIR, app.FAILED_DB), os.path.join(app.DATA_DIR, app.CACHE_DB) ] target = os.path.join( backupDir, app.BACKUP_FILENAME.format( timestamp=time.strftime('%Y%m%d%H%M%S'))) for (path, dirs, files) in os.walk(app.CACHE_DIR, topdown=True): for dirname in dirs: if path == app.CACHE_DIR and dirname not in ['images']: dirs.remove(dirname) for filename in files: source.append(os.path.join(path, filename)) return helpers.backup_config_zip(source, target, app.DATA_DIR) def safe_to_update(self): def db_safe(self): message = { 'equal': { 'type': DEBUG, 'text': u'We can proceed with the update. New update has same DB version' }, 'upgrade': { 'type': WARNING, 'text': u"We can't proceed with the update. New update has a new DB version. Please manually update" }, 'downgrade': { 'type': WARNING, 'text': u"We can't proceed with the update. New update has a old DB version. It's not possible to downgrade" }, } try: result = self.getDBcompare() if result in message: log.log( message[result]['type'], message[result] ['text']) # unpack the result message into a log entry else: log.warning( u"We can't proceed with the update. Unable to check remote DB version. Error: {0}", result) return result in ['equal' ] # add future True results to the list except Exception as error: log.error( u"We can't proceed with the update. Unable to compare DB version. Error: {0!r}", error) return False def postprocessor_safe(): if not app.auto_post_processor_scheduler.action.amActive: log.debug( u'We can proceed with the update. Post-Processor is not running' ) return True else: log.debug( u"We can't proceed with the update. Post-Processor is running" ) return False def showupdate_safe(): if not app.show_update_scheduler.action.amActive: log.debug( u'We can proceed with the update. Shows are not being updated' ) return True else: log.debug( u"We can't proceed with the update. Shows are being updated" ) return False db_safe = db_safe(self) postprocessor_safe = postprocessor_safe() showupdate_safe = showupdate_safe() if db_safe and postprocessor_safe and showupdate_safe: log.debug(u'Proceeding with auto update') return True else: log.debug(u'Auto update aborted') return False def getDBcompare(self): """ Compare the current DB version with the new branch version. :return: 'upgrade', 'equal', or 'downgrade' """ try: self.updater.need_update() cur_hash = str(self.updater.get_newest_commit_hash()) assert len( cur_hash ) == 40, 'Commit hash wrong length: {length} hash: {hash}'.format( length=len(cur_hash), hash=cur_hash) check_url = 'http://cdn.rawgit.com/{org}/{repo}/{commit}/medusa/databases/main_db.py'.format( org=app.GIT_ORG, repo=app.GIT_REPO, commit=cur_hash) response = self.session.get(check_url) # Get remote DB version match_max_db = re.search( r'MAX_DB_VERSION\s*=\s*(?P<version>\d{2,3})', response.text) new_branch_major_db_version = int( match_max_db.group('version')) if match_max_db else None match_minor_db = re.search( r'CURRENT_MINOR_DB_VERSION\s*=\s*(?P<version>\d{1,2})', response.text) new_branch_min_db_version = int( match_minor_db.group('version')) if match_minor_db else None # Check local DB version main_db_con = db.DBConnection() cur_branch_major_db_version, cur_branch_minor_db_version = main_db_con.checkDBVersion( ) if any([ cur_branch_major_db_version is None, cur_branch_minor_db_version is None, new_branch_major_db_version is None, new_branch_min_db_version is None ]): return 'Could not compare database versions, aborting' if new_branch_major_db_version > cur_branch_major_db_version: return 'upgrade' elif new_branch_major_db_version == cur_branch_major_db_version: if new_branch_min_db_version < cur_branch_minor_db_version: return 'downgrade' elif new_branch_min_db_version > cur_branch_minor_db_version: return 'upgrade' return 'equal' else: return 'downgrade' except Exception as e: return repr(e) @staticmethod def find_install_type(): """ Determines how this copy of sr was installed. :return: type of installation. Possible values are: 'win': any compiled windows build 'git': running from source using git 'source': running from source without git """ # check if we're a windows build if app.BRANCH.startswith('build '): install_type = 'win' elif os.path.isdir(os.path.join(app.PROG_DIR, u'.git')): install_type = 'git' else: install_type = 'source' return install_type def check_for_new_version(self, force=False): """ Check the internet for a newer version. :force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced :return: bool, True for new version or False for no new version. """ if not self.updater or (not app.VERSION_NOTIFY and not app.AUTO_UPDATE and not force): log.info( u'Version checking is disabled, not checking for the newest version' ) app.NEWEST_VERSION_STRING = None return False # checking for updates if not app.AUTO_UPDATE: log.info(u'Checking for updates using {0}', self.install_type.upper()) if not self.updater.need_update(): app.NEWEST_VERSION_STRING = None if force: ui.notifications.message('No update needed') log.info(u'No update needed') # no updates needed return False # found updates self.updater.set_newest_text() return self.updater.can_update() def check_for_new_news(self, force=False): """ Check GitHub for the latest news. :return: unicode, a copy of the news :force: ignored """ # Grab a copy of the news log.debug(u'check_for_new_news: Checking GitHub for latest news.') try: news = self.session.get(app.NEWS_URL).text except Exception: log.warning(u'check_for_new_news: Could not load news from repo.') news = '' if not news: return '' try: last_read = datetime.datetime.strptime(app.NEWS_LAST_READ, '%Y-%m-%d') except Exception: last_read = 0 app.NEWS_UNREAD = 0 gotLatest = False for match in re.finditer(r'^####\s*(\d{4}-\d{2}-\d{2})\s*####', news, re.M): if not gotLatest: gotLatest = True app.NEWS_LATEST = match.group(1) try: if datetime.datetime.strptime(match.group(1), '%Y-%m-%d') > last_read: app.NEWS_UNREAD += 1 except Exception: pass return news def need_update(self): if self.updater: return self.updater.need_update() def update(self): if self.updater: # update branch with current config branch value self.updater.branch = app.BRANCH # check for updates if self.updater.need_update(): return self.updater.update() def list_remote_branches(self): if self.updater: app.GIT_REMOTE_BRANCHES = self.updater.list_remote_branches() return app.GIT_REMOTE_BRANCHES def get_branch(self): if self.updater: return self.updater.branch
class Notifier(object): def __init__(self): self.headers = { 'X-Plex-Device-Name': 'Medusa', 'X-Plex-Product': 'Medusa Notifier', 'X-Plex-Client-Identifier': common.USER_AGENT, 'X-Plex-Version': '2016.02.10' } self.session = MedusaSession() @staticmethod def _notify_pht(message, title='Medusa', host=None, username=None, password=None, force=False): # pylint: disable=too-many-arguments """Internal wrapper for the notify_snatch and notify_download functions Args: message: Message body of the notice to send title: Title of the notice to send host: Plex Home Theater(s) host:port username: Plex username password: Plex password force: Used for the Test method to override config safety checks Returns: Returns a list results in the format of host:ip:result The result will either be 'OK' or False, this is used to be parsed by the calling function. """ from . import kodi_notifier # suppress notifications if the notifier is disabled but the notify options are checked if not app.USE_PLEX_CLIENT and not force: return False host = host or app.PLEX_CLIENT_HOST username = username or app.PLEX_CLIENT_USERNAME password = password or app.PLEX_CLIENT_PASSWORD return kodi_notifier._notify_kodi(message, title=title, host=host, username=username, password=password, force=force, dest_app='PLEX') # pylint: disable=protected-access ############################################################################## # Public functions ############################################################################## def notify_snatch(self, ep_name, is_proper): if app.PLEX_NOTIFY_ONSNATCH: self._notify_pht( ep_name, common.notifyStrings[(common.NOTIFY_SNATCH, common.NOTIFY_SNATCH_PROPER)[is_proper]]) def notify_download(self, ep_name): if app.PLEX_NOTIFY_ONDOWNLOAD: self._notify_pht(ep_name, common.notifyStrings[common.NOTIFY_DOWNLOAD]) def notify_subtitle_download(self, ep_name, lang): if app.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD: self._notify_pht( ep_name + ': ' + lang, common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD]) def notify_git_update(self, new_version='??'): if app.NOTIFY_ON_UPDATE: update_text = common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] title = common.notifyStrings[common.NOTIFY_GIT_UPDATE] if update_text and title and new_version: self._notify_pht(update_text + new_version, title) def notify_login(self, ipaddress=''): if app.NOTIFY_ON_LOGIN: update_text = common.notifyStrings[common.NOTIFY_LOGIN_TEXT] title = common.notifyStrings[common.NOTIFY_LOGIN] if update_text and title and ipaddress: self._notify_pht(update_text.format(ipaddress), title) def test_notify_pht(self, host, username, password): return self._notify_pht('This is a test notification from Medusa', 'Test Notification', host, username, password, force=True) def test_notify_pms(self, host, username, password, plex_server_token): return self.update_library(hosts=host, username=username, password=password, plex_server_token=plex_server_token, force=True) def update_library( self, ep_obj=None, hosts=None, # pylint: disable=too-many-arguments, too-many-locals, too-many-statements, too-many-branches username=None, password=None, plex_server_token=None, force=False): """Handles updating the Plex Media Server host via HTTP API Plex Media Server currently only supports updating the whole video library and not a specific path. Returns: Returns None for no issue, else a string of host with connection issues """ if not (app.USE_PLEX_SERVER and app.PLEX_UPDATE_LIBRARY) and not force: return None hosts = hosts or app.PLEX_SERVER_HOST if not hosts: log.debug( u'PLEX: No Plex Media Server host specified, check your settings' ) return False if not self.get_token(username, password, plex_server_token): log.warning( u'PLEX: Error getting auth token for Plex Media Server, check your settings' ) return False file_location = '' if not ep_obj else ep_obj.location gen_hosts = generate(hosts) hosts = {x.strip() for x in gen_hosts if x.strip()} hosts_all = hosts_match = {} hosts_failed = set() for cur_host in hosts: url = 'http{0}://{1}/library/sections'.format( ('', 's')[bool(app.PLEX_SERVER_HTTPS)], cur_host) try: # TODO: SESSION: Check if this needs exception handling. xml_response = self.session.get(url, headers=self.headers).text if not xml_response: log.warning( u'PLEX: Error while trying to contact Plex Media Server: {0}', cur_host) hosts_failed.add(cur_host) continue media_container = etree.fromstring(xml_response) except IOError as error: log.warning( u'PLEX: Error while trying to contact Plex Media Server: {0}', ex(error)) hosts_failed.add(cur_host) continue except Exception as error: if 'invalid token' in str(error): log.warning(u'PLEX: Please set TOKEN in Plex settings: ') else: log.warning( u'PLEX: Error while trying to contact Plex Media Server: {0}', ex(error)) hosts_failed.add(cur_host) continue sections = media_container.findall('.//Directory') if not sections: log.debug(u'PLEX: Plex Media Server not running on: {0}', cur_host) hosts_failed.add(cur_host) continue for section in sections: if 'show' == section.attrib['type']: keyed_host = [(str(section.attrib['key']), cur_host)] hosts_all.update(keyed_host) if not file_location: continue for section_location in section.findall('.//Location'): section_path = re.sub( r'[/\\]+', '/', section_location.attrib['path'].lower()) section_path = re.sub(r'^(.{,2})[/\\]', '', section_path) location_path = re.sub(r'[/\\]+', '/', file_location.lower()) location_path = re.sub(r'^(.{,2})[/\\]', '', location_path) if section_path in location_path: hosts_match.update(keyed_host) if force: return (', '.join(set(hosts_failed)), None)[not len(hosts_failed)] if hosts_match: log.debug( u'PLEX: Updating hosts where TV section paths match the downloaded show: {0}', ', '.join(set(hosts_match))) else: log.debug(u'PLEX: Updating all hosts with TV sections: {0}', ', '.join(set(hosts_all))) hosts_try = (hosts_match.copy(), hosts_all.copy())[not len(hosts_match)] for section_key, cur_host in iteritems(hosts_try): url = 'http{0}://{1}/library/sections/{2}/refresh'.format( ('', 's')[bool(app.PLEX_SERVER_HTTPS)], cur_host, section_key) try: # TODO: Check if this needs exception handling self.session.get(url, headers=self.headers).text except Exception as error: log.warning( u'PLEX: Error updating library section for Plex Media Server: {0}', ex(error)) hosts_failed.add(cur_host) return (', '.join(set(hosts_failed)), None)[not len(hosts_failed)] def get_token(self, username=None, password=None, plex_server_token=None): username = username or app.PLEX_SERVER_USERNAME password = password or app.PLEX_SERVER_PASSWORD plex_server_token = plex_server_token or app.PLEX_SERVER_TOKEN if plex_server_token: self.headers['X-Plex-Token'] = plex_server_token if 'X-Plex-Token' in self.headers: return True if not (username and password): return True log.debug(u'PLEX: fetching plex.tv credentials for user: {0}', username) params = {'user[login]': username, 'user[password]': password} try: response = self.session.post('https://plex.tv/users/sign_in.json', data=params, headers=self.headers).json() self.headers['X-Plex-Token'] = response['user'][ 'authentication_token'] except Exception as error: self.headers.pop('X-Plex-Token', '') log.debug( u'PLEX: Error fetching credentials from from plex.tv for user {0}: {1}', username, error) return 'X-Plex-Token' in self.headers
class Notifier(object): def __init__(self): self.session = MedusaSession() self.session.headers.update({ 'X-Plex-Device-Name': 'Medusa', 'X-Plex-Product': 'Medusa Notifier', 'X-Plex-Client-Identifier': common.USER_AGENT, 'X-Plex-Version': app.APP_VERSION, }) @staticmethod def _notify_pht(title, message, host=None, username=None, password=None, force=False): # pylint: disable=too-many-arguments """Internal wrapper for the notify_snatch and notify_download functions Args: message: Message body of the notice to send title: Title of the notice to send host: Plex Home Theater(s) host:port username: Plex username password: Plex password force: Used for the Test method to override config safety checks Returns: Returns a list results in the format of host:ip:result The result will either be 'OK' or False, this is used to be parsed by the calling function. """ from medusa.notifiers import kodi_notifier # suppress notifications if the notifier is disabled but the notify options are checked if not app.USE_PLEX_CLIENT and not force: return False host = host or app.PLEX_CLIENT_HOST username = username or app.PLEX_CLIENT_USERNAME password = password or app.PLEX_CLIENT_PASSWORD return kodi_notifier._notify_kodi(message, title=title, host=host, username=username, password=password, force=force, dest_app='PLEX') # pylint: disable=protected-access ############################################################################## # Public functions ############################################################################## def notify_snatch(self, title, message): if app.PLEX_NOTIFY_ONSNATCH: self._notify_pht(title, message) def notify_download(self, ep_obj): if app.PLEX_NOTIFY_ONDOWNLOAD: self._notify_pht(common.notifyStrings[common.NOTIFY_DOWNLOAD], ep_obj.pretty_name_with_quality()) def notify_subtitle_download(self, ep_obj, lang): if app.PLEX_NOTIFY_ONSUBTITLEDOWNLOAD: self._notify_pht(common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD], ep_obj.pretty_name() + ': ' + lang) def notify_git_update(self, new_version='??'): if app.NOTIFY_ON_UPDATE: update_text = common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] title = common.notifyStrings[common.NOTIFY_GIT_UPDATE] if update_text and title and new_version: self._notify_pht(title, update_text + new_version) def notify_login(self, ipaddress=''): if app.NOTIFY_ON_LOGIN: update_text = common.notifyStrings[common.NOTIFY_LOGIN_TEXT] title = common.notifyStrings[common.NOTIFY_LOGIN] if update_text and title and ipaddress: self._notify_pht(title, update_text.format(ipaddress)) def test_notify_pht(self, host, username, password): return self._notify_pht('Test Notification', 'This is a test notification from Medusa', host, username, password, force=True) def test_notify_pms(self, host, username, password, plex_server_token): return self.update_library(hosts=host, username=username, password=password, plex_server_token=plex_server_token, force=True) def update_library(self, ep_obj=None, hosts=None, # pylint: disable=too-many-arguments, too-many-locals, too-many-statements, too-many-branches username=None, password=None, plex_server_token=None, force=False): """Handles updating the Plex Media Server host via HTTP API Plex Media Server currently only supports updating the whole video library and not a specific path. Returns: Returns None for no issue, else a string of host with connection issues """ if not (app.USE_PLEX_SERVER and app.PLEX_UPDATE_LIBRARY) and not force: return None hosts = hosts or app.PLEX_SERVER_HOST if not hosts: log.debug(u'PLEX: No Plex Media Server host specified, check your settings') return False if not self.get_token(username, password, plex_server_token): log.warning(u'PLEX: Error getting auth token for Plex Media Server, check your settings') return False file_location = '' if not ep_obj else ep_obj.location gen_hosts = generate(hosts) hosts = (x.strip() for x in gen_hosts if x.strip()) all_hosts = {} matching_hosts = {} failed_hosts = set() schema = 'https' if app.PLEX_SERVER_HTTPS else 'http' for cur_host in hosts: url = '{schema}://{host}/library/sections'.format( schema=schema, host=cur_host ) try: response = self.session.get(url) except requests.RequestException as error: log.warning(u'PLEX: Error while trying to contact Plex Media Server: {0}', ex(error)) failed_hosts.add(cur_host) continue try: response.raise_for_status() except requests.RequestException as error: if response.status_code == 401: log.warning(u'PLEX: Unauthorized. Please set TOKEN or USERNAME and PASSWORD in Plex settings') else: log.warning(u'PLEX: Error while trying to contact Plex Media Server: {0}', ex(error)) failed_hosts.add(cur_host) continue else: xml_response = response.text if not xml_response: log.warning(u'PLEX: Error while trying to contact Plex Media Server: {0}', cur_host) failed_hosts.add(cur_host) continue else: media_container = etree.fromstring(xml_response) sections = media_container.findall('.//Directory') if not sections: log.debug(u'PLEX: Plex Media Server not running on: {0}', cur_host) failed_hosts.add(cur_host) continue for section in sections: if 'show' == section.attrib['type']: key = str(section.attrib['key']) keyed_host = { key: cur_host, } all_hosts.update(keyed_host) if not file_location: continue for section_location in section.findall('.//Location'): section_path = re.sub(r'[/\\]+', '/', section_location.attrib['path'].lower()) section_path = re.sub(r'^(.{,2})[/\\]', '', section_path) location_path = re.sub(r'[/\\]+', '/', file_location.lower()) location_path = re.sub(r'^(.{,2})[/\\]', '', location_path) if section_path in location_path: matching_hosts.update(keyed_host) if force: return ', '.join(failed_hosts) if failed_hosts else None if matching_hosts: hosts_try = matching_hosts result = u'PLEX: Updating hosts where TV section paths match the downloaded show: {0}' else: hosts_try = all_hosts result = u'PLEX: Updating all hosts with TV sections: {0}' log.debug(result.format(', '.join(hosts_try))) for section_key, cur_host in iteritems(hosts_try): url = '{schema}://{host}/library/sections/{key}/refresh'.format( schema=schema, host=cur_host, key=section_key, ) try: response = self.session.get(url) except requests.RequestException as error: log.warning(u'PLEX: Error updating library section for Plex Media Server: {0}', ex(error)) failed_hosts.add(cur_host) else: del response # request succeeded so response is not needed return ', '.join(failed_hosts) if failed_hosts else None def get_token(self, username=None, password=None, plex_server_token=None): """ Get auth token. Try to get the auth token from the argument, the config, the session, or the Plex website in that order. :param username: plex.tv username :param password: plex.tv password :param plex_server_token: auth token :returns: Plex auth token being used or True if authentication is not required, else None """ username = username or app.PLEX_SERVER_USERNAME password = password or app.PLEX_SERVER_PASSWORD plex_server_token = plex_server_token or app.PLEX_SERVER_TOKEN if plex_server_token: self.session.headers['X-Plex-Token'] = plex_server_token if 'X-Plex-Token' in self.session.headers: return self.session.headers['X-Plex-Token'] if not (username and password): return True log.debug(u'PLEX: fetching plex.tv credentials for user: {0}', username) error_msg = u'PLEX: Error fetching credentials from plex.tv for user {0}: {1}' try: # sign in response = self.session.post( 'https://plex.tv/users/sign_in.json', data={ 'user[login]': username, 'user[password]': password, } ) response.raise_for_status() except requests.RequestException as error: log.debug(error_msg, username, error) return try: # get json data data = response.json() except ValueError as error: log.debug(error_msg, username, error) return try: # get token from key plex_server_token = data['user']['authentication_token'] except KeyError as error: log.debug(error_msg, username, error) return else: self.session.headers['X-Plex-Token'] = plex_server_token return self.session.headers.get('X-Plex-Token')