예제 #1
0
파일: plex.py 프로젝트: reconman/Medusa
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')
예제 #2
0
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
예제 #3
0
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
예제 #4
0
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
예제 #5
0
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
예제 #6
0
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
예제 #7
0
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
예제 #8
0
파일: plex.py 프로젝트: pymedusa/SickRage
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')