Exemple #1
0
 def test_preserve_last_key_case(self):
     cid = CaseInsensitiveDict({"Accept": "application/json", "user-Agent": "requests"})
     cid.update({"ACCEPT": "application/json"})
     cid["USER-AGENT"] = "requests"
     keyset = frozenset(["ACCEPT", "USER-AGENT"])
     assert frozenset(i[0] for i in cid.items()) == keyset
     assert frozenset(cid.keys()) == keyset
     assert frozenset(cid) == keyset
Exemple #2
0
 def test_preserve_key_case(self):
     cid = CaseInsensitiveDict({
         'Accept': 'application/json',
         'user-Agent': 'requests',
     })
     keyset = frozenset(['Accept', 'user-Agent'])
     assert frozenset(i[0] for i in cid.items()) == keyset
     assert frozenset(cid.keys()) == keyset
     assert frozenset(cid) == keyset
 def test_preserve_key_case(self):
     cid = CaseInsensitiveDict({
         'Accept': 'application/json',
         'user-Agent': 'requests',
     })
     keyset = frozenset(['Accept', 'user-Agent'])
     assert frozenset(i[0] for i in cid.items()) == keyset
     assert frozenset(cid.keys()) == keyset
     assert frozenset(cid) == keyset
 def test_fixes_649(self):
     """__setitem__ should behave case-insensitively."""
     cid = CaseInsensitiveDict()
     cid['spam'] = 'oneval'
     cid['Spam'] = 'twoval'
     cid['sPAM'] = 'redval'
     cid['SPAM'] = 'blueval'
     assert cid['spam'] == 'blueval'
     assert cid['SPAM'] == 'blueval'
     assert list(cid.keys()) == ['SPAM']
Exemple #5
0
 def test_fixes_649(self):
     """__setitem__ should behave case-insensitively."""
     cid = CaseInsensitiveDict()
     cid["spam"] = "oneval"
     cid["Spam"] = "twoval"
     cid["sPAM"] = "redval"
     cid["SPAM"] = "blueval"
     assert cid["spam"] == "blueval"
     assert cid["SPAM"] == "blueval"
     assert list(cid.keys()) == ["SPAM"]
Exemple #6
0
 def test_fixes_649(self):
     """__setitem__ should behave case-insensitively."""
     cid = CaseInsensitiveDict()
     cid['spam'] = 'oneval'
     cid['Spam'] = 'twoval'
     cid['sPAM'] = 'redval'
     cid['SPAM'] = 'blueval'
     assert cid['spam'] == 'blueval'
     assert cid['SPAM'] == 'blueval'
     assert list(cid.keys()) == ['SPAM']
Exemple #7
0
    def __init__(self,
                 response=None,
                 status=None,
                 headers=None,
                 mimetype=None,
                 content_type=None,
                 direct_passthrough=False):
        headers = CaseInsensitiveDict(headers) if headers is not None else None
        if response is not None and isinstance(
                response, BaseResponse) and response.headers is not None:
            headers = CaseInsensitiveDict(response.headers)

        if headers is None:
            headers = CaseInsensitiveDict()

        h = headers
        h['Access-Control-Allow-Origin'] = headers.get(
            'Access-Control-Allow-Origin', '*')
        h['Access-Control-Allow-Methods'] = headers.get(
            'Access-Control-Allow-Methods',
            "GET, PUT, POST, HEAD, OPTIONS, DELETE")
        h['Access-Control-Max-Age'] = headers.get('Access-Control-Max-Age',
                                                  "21600")
        h['Cache-Control'] = headers.get(
            'Cache-Control', "no-cache, must-revalidate, no-store")
        if 'Access-Control-Allow-Headers' not in headers and len(
                headers.keys()) > 0:
            h['Access-Control-Allow-Headers'] = ', '.join(iterkeys(headers))

        data = None
        if response is not None and isinstance(response, string_types):
            data = response
            response = None

        if response is not None and isinstance(response, BaseResponse):
            new_response_headers = CaseInsensitiveDict(
                response.headers if response.headers is not None else {})
            new_response_headers.update(h)
            response.headers = new_response_headers
            headers = None
            data = response.get_data()

        else:
            headers.update(h)
            headers = dict(headers)

        super(IppResponse,
              self).__init__(response=response,
                             status=status,
                             headers=headers,
                             mimetype=mimetype,
                             content_type=content_type,
                             direct_passthrough=direct_passthrough)
        if data is not None:
            self.set_data(data)
Exemple #8
0
 def test_preserve_last_key_case(self):
     cid = CaseInsensitiveDict({
         'Accept': 'application/json',
         'user-Agent': 'requests',
     })
     cid.update({'ACCEPT': 'application/json'})
     cid['USER-AGENT'] = 'requests'
     keyset = frozenset(['ACCEPT', 'USER-AGENT'])
     assert frozenset(i[0] for i in cid.items()) == keyset
     assert frozenset(cid.keys()) == keyset
     assert frozenset(cid) == keyset
 def test_preserve_last_key_case(self):
     cid = CaseInsensitiveDict({
         'Accept': 'application/json',
         'user-Agent': 'requests',
     })
     cid.update({'ACCEPT': 'application/json'})
     cid['USER-AGENT'] = 'requests'
     keyset = frozenset(['ACCEPT', 'USER-AGENT'])
     assert frozenset(i[0] for i in cid.items()) == keyset
     assert frozenset(cid.keys()) == keyset
     assert frozenset(cid) == keyset
    def test_inject_reported_fields_matches_carrier_fields(self):
        carrier = CaseInsensitiveDict()

        AwsXRayPropagatorTest.XRAY_PROPAGATOR.inject(
            carrier,
            build_test_current_context(),
        )

        injected_keys = set(carrier.keys())

        self.assertEqual(injected_keys,
                         AwsXRayPropagatorTest.XRAY_PROPAGATOR.fields)
Exemple #11
0
    def dict_subset(input_list,
                    dic_data: CaseInsensitiveDict) -> CaseInsensitiveDict:
        for name in input_list:
            if name not in [key.strip().lower() for key in dic_data.keys()]:
                logging.warning(
                    f"provided entry with the name - {name} is not "
                    f"valid The program will disregard this input")
                input_list.remove(name)

        sub_data = {
            key: value
            for key, value in dic_data.items() if key.strip().lower() in
            [item.strip().lower() for item in input_list]
        }

        return CaseInsensitiveDict(data=sub_data)
Exemple #12
0
    def set_next_safe_time(self, headers: CaseInsensitiveDict) -> None:  # type: ignore
        """
        Parse returned headers to get weight.

        The binance API has weights on endpoints and that value differs
        for different users. This function parses headers and gets the weight value.

        :param headers: API response headers.
        """
        weight_regex = re.compile(
            r"X-SAPI-USED-IP-WEIGHT-(?P<num>\d+)(?P<period>[SMHD])",
        )
        period_mapping = {"S": "seconds", "M": "minutes", "H": "hours", "D": "days"}
        for header_name in headers.keys():
            match = weight_regex.match(header_name)
            if match:
                delta_param = {
                    period_mapping[match.group("period")]: int(match.group("num")),
                }
                self._next_query_time = datetime.now() + timedelta(**delta_param)
                return
Exemple #13
0
class StateMachine(object):
    """ Helper class that tracks the state of different entities. """
    def __init__(self, bus):
        self._states = CaseInsensitiveDict()
        self._bus = bus
        self._lock = threading.Lock()

    def entity_ids(self, domain_filter=None):
        """ List of entity ids that are being tracked. """
        if domain_filter is not None:
            domain_filter = domain_filter.lower()

            return [
                state.entity_id for key, state in self._states.lower_items()
                if util.split_entity_id(key)[0] == domain_filter
            ]
        else:
            return list(self._states.keys())

    def all(self):
        """ Returns a list of all states. """
        return [state.copy() for state in self._states.values()]

    def get(self, entity_id):
        """ Returns the state of the specified entity. """
        state = self._states.get(entity_id)

        # Make a copy so people won't mutate the state
        return state.copy() if state else None

    def get_since(self, point_in_time):
        """
        Returns all states that have been changed since point_in_time.
        """
        point_in_time = util.strip_microseconds(point_in_time)

        with self._lock:
            return [
                state for state in self._states.values()
                if state.last_updated >= point_in_time
            ]

    def is_state(self, entity_id, state):
        """ Returns True if entity exists and is specified state. """
        return (entity_id in self._states
                and self._states[entity_id].state == state)

    def remove(self, entity_id):
        """ Removes an entity from the state machine.

        Returns boolean to indicate if an entity was removed. """
        with self._lock:
            return self._states.pop(entity_id, None) is not None

    def set(self, entity_id, new_state, attributes=None):
        """ Set the state of an entity, add entity if it does not exist.

        Attributes is an optional dict to specify attributes of this state.

        If you just update the attributes and not the state, last changed will
        not be affected.
        """

        new_state = str(new_state)
        attributes = attributes or {}

        with self._lock:
            old_state = self._states.get(entity_id)

            is_existing = old_state is not None
            same_state = is_existing and old_state.state == new_state
            same_attr = is_existing and old_state.attributes == attributes

            # If state did not exist or is different, set it
            if not (same_state and same_attr):
                last_changed = old_state.last_changed if same_state else None

                state = self._states[entity_id] = \
                    State(entity_id, new_state, attributes, last_changed)

                event_data = {'entity_id': entity_id, 'new_state': state}

                if old_state:
                    event_data['old_state'] = old_state

                self._bus.fire(EVENT_STATE_CHANGED, event_data)

    def track_change(self, entity_ids, action, from_state=None, to_state=None):
        """
        Track specific state changes.
        entity_ids, from_state and to_state can be string or list.
        Use list to match multiple.

        Returns the listener that listens on the bus for EVENT_STATE_CHANGED.
        Pass the return value into hass.bus.remove_listener to remove it.
        """
        from_state = _process_match_param(from_state)
        to_state = _process_match_param(to_state)

        # Ensure it is a lowercase list with entity ids we want to match on
        if isinstance(entity_ids, str):
            entity_ids = (entity_ids.lower(), )
        else:
            entity_ids = tuple(entity_id.lower() for entity_id in entity_ids)

        @ft.wraps(action)
        def state_listener(event):
            """ The listener that listens for specific state changes. """
            if event.data['entity_id'].lower() in entity_ids and \
                    'old_state' in event.data and \
                    _matcher(event.data['old_state'].state, from_state) and \
                    _matcher(event.data['new_state'].state, to_state):

                action(event.data['entity_id'], event.data['old_state'],
                       event.data['new_state'])

        self._bus.listen(EVENT_STATE_CHANGED, state_listener)

        return state_listener
Exemple #14
0
    def reload_config(self, config):
        self._superuser = config['authentication'].get('superuser', {})
        server_parameters = self.get_server_parameters(config)

        conf_changed = hba_changed = ident_changed = local_connection_address_changed = pending_restart = False
        if self._postgresql.state == 'running':
            changes = CaseInsensitiveDict({p: v for p, v in server_parameters.items() if '.' not in p})
            changes.update({p: None for p in self._server_parameters.keys() if not ('.' in p or p in changes)})
            if changes:
                # XXX: query can raise an exception
                for r in self._postgresql.query(('SELECT name, setting, unit, vartype, context '
                                                 + 'FROM pg_catalog.pg_settings ' +
                                                 ' WHERE pg_catalog.lower(name) IN ('
                                                 + ', '.join(['%s'] * len(changes)) +
                                                 ')'), *(k.lower() for k in changes.keys())):
                    if r[4] != 'internal' and r[0] in changes:
                        new_value = changes.pop(r[0])
                        if new_value is None or not compare_values(r[3], r[2], r[1], new_value):
                            if r[4] == 'postmaster':
                                pending_restart = True
                                logger.info('Changed %s from %s to %s (restart required)', r[0], r[1], new_value)
                                if config.get('use_unix_socket') and r[0] == 'unix_socket_directories'\
                                        or r[0] in ('listen_addresses', 'port'):
                                    local_connection_address_changed = True
                            else:
                                logger.info('Changed %s from %s to %s', r[0], r[1], new_value)
                                conf_changed = True
                for param in changes:
                    if param in server_parameters:
                        logger.warning('Removing invalid parameter `%s` from postgresql.parameters', param)
                        server_parameters.pop(param)

            # Check that user-defined-paramters have changed (parameters with period in name)
            if not conf_changed:
                for p, v in server_parameters.items():
                    if '.' in p and (p not in self._server_parameters or str(v) != str(self._server_parameters[p])):
                        logger.info('Changed %s from %s to %s', p, self._server_parameters.get(p), v)
                        conf_changed = True
                        break
                if not conf_changed:
                    for p, v in self._server_parameters.items():
                        if '.' in p and (p not in server_parameters or str(v) != str(server_parameters[p])):
                            logger.info('Changed %s from %s to %s', p, v, server_parameters.get(p))
                            conf_changed = True
                            break

            if not server_parameters.get('hba_file') and config.get('pg_hba'):
                hba_changed = self._config.get('pg_hba', []) != config['pg_hba']

            if not server_parameters.get('ident_file') and config.get('pg_ident'):
                ident_changed = self._config.get('pg_ident', []) != config['pg_ident']

        self._config = config
        self._postgresql.set_pending_restart(pending_restart)
        self._server_parameters = server_parameters
        self._adjust_recovery_parameters()
        self._connect_address = config.get('connect_address')
        self._krbsrvname = config.get('krbsrvname')

        # for not so obvious connection attempts that may happen outside of pyscopg2
        if self._krbsrvname:
            os.environ['PGKRBSRVNAME'] = self._krbsrvname

        if not local_connection_address_changed:
            self.resolve_connection_addresses()

        if conf_changed:
            self.write_postgresql_conf()

        if hba_changed:
            self.replace_pg_hba()

        if ident_changed:
            self.replace_pg_ident()

        if conf_changed or hba_changed or ident_changed:
            logger.info('PostgreSQL configuration items changed, reloading configuration.')
            self._postgresql.reload()
        elif not pending_restart:
            logger.info('No PostgreSQL configuration items changed, nothing to reload.')
Exemple #15
0
    def make_request(self, method, bucket, key=None, params=None, data=None,
                     headers=None):
        # Remove params that are set to None
        if isinstance(params, dict):
            for k, v in params.copy().items():
                if v is None:
                    params.pop(k)

        # Construct target url
        url = 'http://{}.{}'.format(bucket, self.hostname)
        url += '/{}'.format(key) if key is not None else '/'
        if isinstance(params, dict) and len(params) > 0:
            url += '?{}'.format(urllib.urlencode(params))
        elif isinstance(params, basestring):
            url += '?{}'.format(params)

        # Make headers case insensitive
        if headers is None:
            headers = {}
        headers = CaseInsensitiveDict(headers)

        headers['Host'] = '{}.{}'.format(bucket, self.hostname)

        if data is not None:
            try:
                raw_md5 = utils.f_md5(data)
            except:
                m = hashlib.md5()
                m.update(data)
                raw_md5 = m.digest()
            md5 = b64encode(raw_md5)
            headers['Content-MD5'] = md5
        else:
            md5 = ''

        try:
            content_type = headers['Content-Type']
        except KeyError:
            content_type = ''

        date = formatdate(timeval=None, localtime=False, usegmt=True)
        headers['x-amz-date'] = date

        # Construct canonicalized amz headers string
        canonicalized_amz_headers = ''
        amz_keys = [k for k in list(headers.keys()) if k.startswith('x-amz-')]
        for k in sorted(amz_keys):
            v = headers[k].strip()
            canonicalized_amz_headers += '{}:{}\n'.format(k.lower(), v)

        # Construct canonicalized resource string
        canonicalized_resource = '/' + bucket
        canonicalized_resource += '/' if key is None else '/{}'.format(key)
        if isinstance(params, basestring):
            canonicalized_resource += '?{}'.format(params)
        elif isinstance(params, dict) and len(params) > 0:
            canonicalized_resource += '?{}'.format(urllib.urlencode(params))

        # Construct string to sign
        string_to_sign = method.upper() + '\n'
        string_to_sign += md5 + '\n'
        string_to_sign += content_type + '\n'
        string_to_sign += '\n'  # date is always set through x-amz-date
        string_to_sign += canonicalized_amz_headers + canonicalized_resource

        # Create signature
        h = hmac.new(self.secret_access_key, string_to_sign, hashlib.sha1)
        signature = b64encode(h.digest())

        # Set authorization header
        auth_head = 'AWS {}:{}'.format(self.access_key_id, signature)
        headers['Authorization'] = auth_head

        # Prepare Request
        req = Request(method, url, data=data, headers=headers).prepare()

        # Log request data.
        # Prepare request beforehand so requests-altered headers show.
        # Combine into a single message so we don't have to bother with
        # locking to make lines appear together.
        log_message = '{} {}\n'.format(method, url)
        log_message += 'headers:'
        for k in sorted(req.headers.keys()):
            log_message += '\n {}: {}'.format(k, req.headers[k])
        log.debug(log_message)

        # Send request
        resp = Session().send(req)

        # Update stats, log response data.
        self.stats[method.upper()] += 1
        log.debug('response: {} ({} {})'.format(resp.status_code, method, url))

        # Handle errors
        if resp.status_code/100 != 2:
            soup = BeautifulSoup(resp.text)
            error = soup.find('error')

            log_message = "S3 replied with non 2xx response code!!!!\n"
            log_message += '  request: {} {}\n'.format(method, url)
            for c in error.children:
                error_name = c.name
                error_message = c.text.encode('unicode_escape')
                log_message += '  {}: {}\n'.format(error_name, error_message)
            log.debug(log_message)

            code = error.find('code').text
            message = error.find('message').text
            raise S3ResponseError(code, message, resp)

        return resp
Exemple #16
0
class BlinkSyncModule():
    """Class to initialize sync module."""

    def __init__(self, blink, network_name, network_id, camera_list):
        """
        Initialize Blink sync module.

        :param blink: Blink class instantiation
        """
        self.blink = blink
        self._auth_header = blink.auth_header
        self.network_id = network_id
        self.region = blink.region
        self.region_id = blink.region_id
        self.name = network_name
        self.serial = None
        self.status = None
        self.sync_id = None
        self.host = None
        self.summary = None
        self.network_info = None
        self.events = []
        self.cameras = CaseInsensitiveDict({})
        self.motion_interval = blink.motion_interval
        self.motion = {}
        self.last_record = {}
        self.camera_list = camera_list

    @property
    def attributes(self):
        """Return sync attributes."""
        attr = {
            'name': self.name,
            'id': self.sync_id,
            'network_id': self.network_id,
            'serial': self.serial,
            'status': self.status,
            'region': self.region,
            'region_id': self.region_id,
        }
        return attr

    @property
    def urls(self):
        """Return device urls."""
        return self.blink.urls

    @property
    def online(self):
        """Return boolean system online status."""
        return ONLINE[self.status]

    @property
    def arm(self):
        """Return status of sync module: armed/disarmed."""
        try:
            return self.network_info['network']['armed']
        except (KeyError, TypeError):
            return None

    @arm.setter
    def arm(self, value):
        """Arm or disarm system."""
        if value:
            return api.request_system_arm(self.blink, self.network_id)

        return api.request_system_disarm(self.blink, self.network_id)

    def start(self):
        """Initialize the system."""
        response = api.request_syncmodule(self.blink,
                                          self.network_id)
        try:
            self.summary = response['syncmodule']
            self.network_id = self.summary['network_id']
        except (TypeError, KeyError):
            _LOGGER.error(("Could not retrieve sync module information "
                           "with response: %s"), response, exc_info=True)
            return False

        try:
            self.sync_id = self.summary['id']
            self.serial = self.summary['serial']
            self.status = self.summary['status']
        except KeyError:
            _LOGGER.error("Could not extract some sync module info: %s",
                          response,
                          exc_info=True)

        self.network_info = api.request_network_status(self.blink,
                                                       self.network_id)

        self.check_new_videos()
        try:
            for camera_config in self.camera_list:
                if 'name' not in camera_config:
                    break
                name = camera_config['name']
                self.cameras[name] = BlinkCamera(self)
                self.motion[name] = False
                camera_info = self.get_camera_info(camera_config['id'])
                self.cameras[name].update(camera_info,
                                          force_cache=True,
                                          force=True)
        except KeyError:
            _LOGGER.error("Could not create cameras instances for %s",
                          self.name,
                          exc_info=True)
            return False

        return True

    def get_events(self, **kwargs):
        """Retrieve events from server."""
        force = kwargs.pop('force', False)
        response = api.request_sync_events(self.blink,
                                           self.network_id,
                                           force=force)
        try:
            return response['event']
        except (TypeError, KeyError):
            _LOGGER.error("Could not extract events: %s",
                          response,
                          exc_info=True)
            return False

    def get_camera_info(self, camera_id):
        """Retrieve camera information."""
        response = api.request_camera_info(self.blink,
                                           self.network_id,
                                           camera_id)
        try:
            return response['camera'][0]
        except (TypeError, KeyError):
            _LOGGER.error("Could not extract camera info: %s",
                          response,
                          exc_info=True)
            return []

    def refresh(self, force_cache=False):
        """Get all blink cameras and pulls their most recent status."""
        self.network_info = api.request_network_status(self.blink,
                                                       self.network_id)
        self.check_new_videos()
        for camera_name in self.cameras.keys():
            camera_id = self.cameras[camera_name].camera_id
            camera_info = self.get_camera_info(camera_id)
            self.cameras[camera_name].update(camera_info,
                                             force_cache=force_cache)

    def check_new_videos(self):
        """Check if new videos since last refresh."""
        try:
            interval = self.blink.last_refresh - self.motion_interval*60
        except TypeError:
            # This is the first start, so refresh hasn't happened yet.
            # No need to check for motion.
            return False

        resp = api.request_videos(self.blink,
                                  time=interval,
                                  page=1)

        for camera in self.cameras.keys():
            self.motion[camera] = False

        try:
            info = resp['media']
        except (KeyError, TypeError):
            _LOGGER.warning("Could not check for motion. Response: %s", resp)
            return False

        for entry in info:
            try:
                name = entry['device_name']
                clip = entry['media']
                timestamp = entry['created_at']
                self.motion[name] = True
                self.last_record[name] = {'clip': clip, 'time': timestamp}
            except KeyError:
                _LOGGER.debug("No new videos since last refresh.")

        return True
Exemple #17
0
 def test_preserve_key_case(self):
     cid = CaseInsensitiveDict({"Accept": "application/json", "user-Agent": "requests"})
     keyset = frozenset(["Accept", "user-Agent"])
     assert frozenset(i[0] for i in cid.items()) == keyset
     assert frozenset(cid.keys()) == keyset
     assert frozenset(cid) == keyset
Exemple #18
0
class tizgmusicproxy(object):
    """A class for logging into a Google Play Music account and retrieving song
    URLs.

    """

    all_songs_album_title = "All Songs"
    thumbs_up_playlist_name = "Thumbs Up"

    def __init__(self, email, password, device_id):
        self.__gmusic = Mobileclient()
        self.__email = email
        self.__device_id = device_id
        self.logged_in = False
        self.queue = list()
        self.queue_index = -1
        self.play_queue_order = list()
        self.play_modes = TizEnumeration(["NORMAL", "SHUFFLE"])
        self.current_play_mode = self.play_modes.NORMAL
        self.now_playing_song = None

        userdir = os.path.expanduser('~')
        tizconfig = os.path.join(userdir, ".config/tizonia/." + email + ".auth_token")
        auth_token = ""
        if os.path.isfile(tizconfig):
            with open(tizconfig, "r") as f:
                auth_token = pickle.load(f)
                if auth_token:
                    # 'Keep track of the auth token' workaround. See:
                    # https://github.com/diraimondo/gmusicproxy/issues/34#issuecomment-147359198
                    print_msg("[Google Play Music] [Authenticating] : " \
                              "'with cached auth token'")
                    self.__gmusic.android_id = device_id
                    self.__gmusic.session._authtoken = auth_token
                    self.__gmusic.session.is_authenticated = True
                    try:
                        self.__gmusic.get_registered_devices()
                    except CallFailure:
                        # The token has expired. Reset the client object
                        print_wrn("[Google Play Music] [Authenticating] : " \
                                  "'auth token expired'")
                        self.__gmusic = Mobileclient()
                        auth_token = ""

        if not auth_token:
            attempts = 0
            print_nfo("[Google Play Music] [Authenticating] : " \
                      "'with user credentials'")
            while not self.logged_in and attempts < 3:
                self.logged_in = self.__gmusic.login(email, password, device_id)
                attempts += 1

            with open(tizconfig, "a+") as f:
                f.truncate()
                pickle.dump(self.__gmusic.session._authtoken, f)

        self.library = CaseInsensitiveDict()
        self.song_map = CaseInsensitiveDict()
        self.playlists = CaseInsensitiveDict()
        self.stations = CaseInsensitiveDict()

    def logout(self):
        """ Reset the session to an unauthenticated, default state.

        """
        self.__gmusic.logout()

    def set_play_mode(self, mode):
        """ Set the playback mode.

        :param mode: curren tvalid values are "NORMAL" and "SHUFFLE"

        """
        self.current_play_mode = getattr(self.play_modes, mode)
        self.__update_play_queue_order()

    def current_song_title_and_artist(self):
        """ Retrieve the current track's title and artist name.

        """
        logging.info("current_song_title_and_artist")
        song = self.now_playing_song
        if song:
            title = to_ascii(self.now_playing_song.get('title'))
            artist = to_ascii(self.now_playing_song.get('artist'))
            logging.info("Now playing %s by %s", title, artist)
            return artist, title
        else:
            return '', ''

    def current_song_album_and_duration(self):
        """ Retrieve the current track's album and duration.

        """
        logging.info("current_song_album_and_duration")
        song = self.now_playing_song
        if song:
            album = to_ascii(self.now_playing_song.get('album'))
            duration = to_ascii \
                       (self.now_playing_song.get('durationMillis'))
            logging.info("album %s duration %s", album, duration)
            return album, int(duration)
        else:
            return '', 0

    def current_track_and_album_total(self):
        """Return the current track number and the total number of tracks in the
        album, if known.

        """
        logging.info("current_track_and_album_total")
        song = self.now_playing_song
        track = 0
        total = 0
        if song:
            try:
                track = self.now_playing_song['trackNumber']
                total = self.now_playing_song['totalTrackCount']
                logging.info("track number %s total tracks %s", track, total)
            except KeyError:
                logging.info("trackNumber or totalTrackCount : not found")
        else:
            logging.info("current_song_track_number_"
                         "and_total_tracks : not found")
        return track, total

    def current_song_year(self):
        """ Return the current track's year of publication.

        """
        logging.info("current_song_year")
        song = self.now_playing_song
        year = 0
        if song:
            try:
                year = song['year']
                logging.info("track year %s", year)
            except KeyError:
                logging.info("year : not found")
        else:
            logging.info("current_song_year : not found")
        return year

    def clear_queue(self):
        """ Clears the playback queue.

        """
        self.queue = list()
        self.queue_index = -1

    def enqueue_artist(self, arg):
        """ Search the user's library for tracks from the given artist and adds
        them to the playback queue.

        :param arg: an artist
        """
        try:
            self.__update_local_library()
            artist = None
            if arg not in self.library.keys():
                for name, art in self.library.iteritems():
                    if arg.lower() in name.lower():
                        artist = art
                        print_wrn("[Google Play Music] '{0}' not found. " \
                                  "Playing '{1}' instead." \
                                  .format(arg.encode('utf-8'), \
                                          name.encode('utf-8')))
                        break
                if not artist:
                    # Play some random artist from the library
                    random.seed()
                    artist = random.choice(self.library.keys())
                    artist = self.library[artist]
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))
            else:
                artist = self.library[arg]
            tracks_added = 0
            for album in artist:
                tracks_added += self.__enqueue_tracks(artist[album])
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(artist)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Artist not found : {0}".format(arg))

    def enqueue_album(self, arg):
        """ Search the user's library for albums with a given name and adds
        them to the playback queue.

        """
        try:
            self.__update_local_library()
            album = None
            artist = None
            tentative_album = None
            tentative_artist = None
            for library_artist in self.library:
                for artist_album in self.library[library_artist]:
                    print_nfo("[Google Play Music] [Album] '{0}'." \
                              .format(to_ascii(artist_album)))
                    if not album:
                        if arg.lower() == artist_album.lower():
                            album = artist_album
                            artist = library_artist
                            break
                    if not tentative_album:
                        if arg.lower() in artist_album.lower():
                            tentative_album = artist_album
                            tentative_artist = library_artist
                if album:
                    break

            if not album and tentative_album:
                album = tentative_album
                artist = tentative_artist
                print_wrn("[Google Play Music] '{0}' not found. " \
                          "Playing '{1}' instead." \
                          .format(arg.encode('utf-8'), \
                          album.encode('utf-8')))
            if not album:
                # Play some random album from the library
                random.seed()
                artist = random.choice(self.library.keys())
                album = random.choice(self.library[artist].keys())
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            if not album:
                raise KeyError("Album not found : {0}".format(arg))

            self.__enqueue_tracks(self.library[artist][album])
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(album)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Album not found : {0}".format(arg))

    def enqueue_playlist(self, arg):
        """Search the user's library for playlists with a given name
        and adds the tracks of the first match to the playback queue.

        Requires Unlimited subscription.

        """
        try:
            self.__update_local_library()
            self.__update_playlists()
            self.__update_playlists_unlimited()
            playlist = None
            playlist_name = None
            for name, plist in self.playlists.items():
                print_nfo("[Google Play Music] [Playlist] '{0}'." \
                          .format(to_ascii(name)))
            if arg not in self.playlists.keys():
                for name, plist in self.playlists.iteritems():
                    if arg.lower() in name.lower():
                        playlist = plist
                        playlist_name = name
                        print_wrn("[Google Play Music] '{0}' not found. " \
                                  "Playing '{1}' instead." \
                                  .format(arg.encode('utf-8'), \
                                          to_ascii(name)))
                        break
                if not playlist:
                    # Play some random playlist from the library
                    random.seed()
                    playlist_name = random.choice(self.playlists.keys())
                    playlist = self.playlists[playlist_name]
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))
            else:
                playlist_name = arg
                playlist = self.playlists[arg]

            self.__enqueue_tracks(playlist)
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(playlist_name)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Playlist not found : {0}".format(arg))

    def enqueue_station_unlimited(self, arg):
        """Search the user's library for a station with a given name
        and add its tracks to the playback queue.

        Requires Unlimited subscription.

        """
        try:
            # First try to find a suitable station in the user's library
            self.__enqueue_user_station_unlimited(arg)

            if not len(self.queue):
                # If no suitable station is found in the user's library, then
                # search google play unlimited for a potential match.
                self.__enqueue_station_unlimited(arg)

            if not len(self.queue):
                raise KeyError

        except KeyError:
            raise KeyError("Station not found : {0}".format(arg))

    def enqueue_genre_unlimited(self, arg):
        """Search Unlimited for a genre with a given name and add its
        tracks to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving genres] : '{0}'. " \
                  .format(self.__email))

        try:
            all_genres = list()
            root_genres = self.__gmusic.get_genres()
            second_tier_genres = list()
            for root_genre in root_genres:
                second_tier_genres += self.__gmusic.get_genres(root_genre['id'])
            all_genres += root_genres
            all_genres += second_tier_genres
            for genre in all_genres:
                print_nfo("[Google Play Music] [Genre] '{0}'." \
                          .format(to_ascii(genre['name'])))
            genre = dict()
            if arg not in all_genres:
                genre = next((g for g in all_genres \
                              if arg.lower() in to_ascii(g['name']).lower()), \
                             None)

            tracks_added = 0
            while not tracks_added:
                if not genre and len(all_genres):
                    # Play some random genre from the search results
                    random.seed()
                    genre = random.choice(all_genres)
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))

                genre_name = genre['name']
                genre_id = genre['id']
                station_id = self.__gmusic.create_station(genre_name, \
                                                          None, None, None, genre_id)
                num_tracks = 200
                tracks = self.__gmusic.get_station_tracks(station_id, num_tracks)
                tracks_added = self.__enqueue_tracks(tracks)
                logging.info("Added %d tracks from %s to queue", tracks_added, genre_name)
                if not tracks_added:
                    # This will produce another iteration in the loop
                    genre = None

            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(genre['name'])))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Genre not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_situation_unlimited(self, arg):
        """Search Unlimited for a situation with a given name and add its
        tracks to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving situations] : '{0}'. " \
                  .format(self.__email))

        try:

            self.__enqueue_situation_unlimited(arg)

            if not len(self.queue):
                raise KeyError

            logging.info("Added %d tracks from %s to queue", \
                         len(self.queue), arg)

        except KeyError:
            raise KeyError("Situation not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_artist_unlimited(self, arg):
        """Search Unlimited for an artist and adds the artist's 200 top tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        try:
            artist = self.__gmusic_search(arg, 'artist')

            include_albums = False
            max_top_tracks = 200
            max_rel_artist = 0
            artist_tracks = dict()
            if artist:
                artist_tracks = self.__gmusic.get_artist_info \
                                (artist['artist']['artistId'],
                                 include_albums, max_top_tracks,
                                 max_rel_artist)['topTracks']
            if not artist_tracks:
                raise KeyError

            tracks_added = self.__enqueue_tracks(artist_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Artist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_album_unlimited(self, arg):
        """Search Unlimited for an album and add its tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        try:
            album = self.__gmusic_search(arg, 'album')
            album_tracks = dict()
            if album:
                album_tracks = self.__gmusic.get_album_info \
                               (album['album']['albumId'])['tracks']
            if not album_tracks:
                raise KeyError

            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format((album['album']['name']).encode('utf-8')))

            tracks_added = self.__enqueue_tracks(album_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Album not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_tracks_unlimited(self, arg):
        """ Search Unlimited for a track name and adds all the matching tracks
        to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving library] : '{0}'. " \
                  .format(self.__email))

        try:
            max_results = 200
            track_hits = self.__gmusic.search(arg, max_results)['song_hits']
            if not len(track_hits):
                # Do another search with an empty string
                track_hits = self.__gmusic.search("", max_results)['song_hits']
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            tracks = list()
            for hit in track_hits:
                tracks.append(hit['track'])
            tracks_added = self.__enqueue_tracks(tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Playlist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_promoted_tracks_unlimited(self):
        """ Retrieve the url of the next track in the playback queue.

        """
        try:
            tracks = self.__gmusic.get_promoted_songs()
            count = 0
            for track in tracks:
                store_track = self.__gmusic.get_track_info(track['storeId'])
                if u'id' not in store_track.keys():
                    store_track[u'id'] = store_track['nid']
                self.queue.append(store_track)
                count += 1
            if count == 0:
                print_wrn("[Google Play Music] Operation requires " \
                          "an Unlimited subscription.")
            logging.info("Added %d Unlimited promoted tracks to queue", \
                         count)
            self.__update_play_queue_order()
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def next_url(self):
        """ Retrieve the url of the next track in the playback queue.

        """
        if len(self.queue):
            self.queue_index += 1
            if (self.queue_index < len(self.queue)) \
               and (self.queue_index >= 0):
                next_song = self.queue[self.play_queue_order[self.queue_index]]
                return self.__retrieve_track_url(next_song)
            else:
                self.queue_index = -1
                return self.next_url()
        else:
            return ''

    def prev_url(self):
        """ Retrieve the url of the previous track in the playback queue.

        """
        if len(self.queue):
            self.queue_index -= 1
            if (self.queue_index < len(self.queue)) \
               and (self.queue_index >= 0):
                prev_song = self.queue[self.play_queue_order[self.queue_index]]
                return self.__retrieve_track_url(prev_song)
            else:
                self.queue_index = len(self.queue)
                return self.prev_url()
        else:
            return ''

    def __update_play_queue_order(self):
        """ Update the queue playback order.

        A sequential order is applied if the current play mode is "NORMAL" or a
        random order if current play mode is "SHUFFLE"

        """
        total_tracks = len(self.queue)
        if total_tracks:
            if not len(self.play_queue_order):
                # Create a sequential play order, if empty
                self.play_queue_order = range(total_tracks)
            if self.current_play_mode == self.play_modes.SHUFFLE:
                random.shuffle(self.play_queue_order)
            print_nfo("[Google Play Music] [Tracks in queue] '{0}'." \
                      .format(total_tracks))

    def __retrieve_track_url(self, song):
        """ Retrieve a song url

        """
        song_url = self.__gmusic.get_stream_url(song['id'], self.__device_id)
        try:
            self.now_playing_song = song
            return song_url
        except AttributeError:
            logging.info("Could not retrieve the song url!")
            raise

    def __update_local_library(self):
        """ Retrieve the songs and albums from the user's library

        """
        print_msg("[Google Play Music] [Retrieving library] : '{0}'. " \
                  .format(self.__email))

        songs = self.__gmusic.get_all_songs()
        self.playlists[self.thumbs_up_playlist_name] = list()

        # Retrieve the user's song library
        for song in songs:
            if "rating" in song and song['rating'] == "5":
                self.playlists[self.thumbs_up_playlist_name].append(song)

            song_id = song['id']
            song_artist = song['artist']
            song_album = song['album']

            self.song_map[song_id] = song

            if song_artist == "":
                song_artist = "Unknown Artist"

            if song_album == "":
                song_album = "Unknown Album"

            if song_artist not in self.library:
                self.library[song_artist] = CaseInsensitiveDict()
                self.library[song_artist][self.all_songs_album_title] = list()

            if song_album not in self.library[song_artist]:
                self.library[song_artist][song_album] = list()

            self.library[song_artist][song_album].append(song)
            self.library[song_artist][self.all_songs_album_title].append(song)

        # Sort albums by track number
        for artist in self.library.keys():
            logging.info("Artist : %s", to_ascii(artist))
            for album in self.library[artist].keys():
                logging.info("   Album : %s", to_ascii(album))
                if album == self.all_songs_album_title:
                    sorted_album = sorted(self.library[artist][album],
                                          key=lambda k: k['title'])
                else:
                    sorted_album = sorted(self.library[artist][album],
                                          key=lambda k: k.get('trackNumber',
                                                              0))
                self.library[artist][album] = sorted_album

    def __update_stations_unlimited(self):
        """ Retrieve stations (Unlimited)

        """
        self.stations.clear()
        stations = self.__gmusic.get_all_stations()
        self.stations[u"I'm Feeling Lucky"] = 'IFL'
        for station in stations:
            station_name = station['name']
            logging.info("station name : %s", to_ascii(station_name))
            self.stations[station_name] = station['id']

    def __enqueue_user_station_unlimited(self, arg):
        """ Enqueue a user station (Unlimited)

        """
        print_msg("[Google Play Music] [Station search "\
                  "in user's library] : '{0}'. " \
                  .format(self.__email))
        self.__update_stations_unlimited()
        station_name = arg
        station_id = None
        for name, st_id in self.stations.iteritems():
            print_nfo("[Google Play Music] [Station] '{0}'." \
                      .format(to_ascii(name)))
        if arg not in self.stations.keys():
            for name, st_id in self.stations.iteritems():
                if arg.lower() in name.lower():
                    station_id = st_id
                    station_name = name
                    break
        else:
            station_id = self.stations[arg]

        num_tracks = 200
        tracks = list()
        if station_id:
            try:
                tracks = self.__gmusic.get_station_tracks(station_id, \
                                                          num_tracks)
            except KeyError:
                raise RuntimeError("Operation requires an "
                                   "Unlimited subscription.")
            tracks_added = self.__enqueue_tracks(tracks)
            if tracks_added:
                if arg != station_name:
                    print_wrn("[Google Play Music] '{0}' not found. " \
                              "Playing '{1}' instead." \
                              .format(arg.encode('utf-8'), name.encode('utf-8')))
                logging.info("Added %d tracks from %s to queue", tracks_added, arg)
                self.__update_play_queue_order()
            else:
                print_wrn("[Google Play Music] '{0}' has no tracks. " \
                          .format(station_name))

        if not len(self.queue):
            print_wrn("[Google Play Music] '{0}' " \
                      "not found in the user's library. " \
                      .format(arg.encode('utf-8')))

    def __enqueue_station_unlimited(self, arg, max_results=200, quiet=False):
        """Search for a station and enqueue all of its tracks (Unlimited)

        """
        if not quiet:
            print_msg("[Google Play Music] [Station search in "\
                      "Google Play Music] : '{0}'. " \
                      .format(arg.encode('utf-8')))
        try:
            station_name = arg
            station_id = None
            station = self.__gmusic_search(arg, 'station', max_results, quiet)

            if station:
                station = station['station']
                station_name = station['name']
                seed = station['seed']
                seed_type = seed['seedType']
                track_id = seed['trackId'] if seed_type == u'2' else None
                artist_id = seed['artistId'] if seed_type == u'3' else None
                album_id = seed['albumId'] if seed_type == u'4' else None
                genre_id = seed['genreId'] if seed_type == u'5' else None
                playlist_token = seed['playlistShareToken'] if seed_type == u'8' else None
                curated_station_id = seed['curatedStationId'] if seed_type == u'9' else None
                num_tracks = max_results
                tracks = list()
                try:
                    station_id \
                        = self.__gmusic.create_station(station_name, \
                                                       track_id, \
                                                       artist_id, \
                                                       album_id, \
                                                       genre_id, \
                                                       playlist_token, \
                                                       curated_station_id)
                    tracks \
                        = self.__gmusic.get_station_tracks(station_id, \
                                                           num_tracks)
                except KeyError:
                    raise RuntimeError("Operation requires an "
                                       "Unlimited subscription.")
                tracks_added = self.__enqueue_tracks(tracks)
                if tracks_added:
                    if not quiet:
                        print_wrn("[Google Play Music] [Station] : '{0}'." \
                                  .format(station_name.encode('utf-8')))
                    logging.info("Added %d tracks from %s to queue", \
                                 tracks_added, arg.encode('utf-8'))
                    self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Station not found : {0}".format(arg))

    def __enqueue_situation_unlimited(self, arg):
        """Search for a situation and enqueue all of its tracks (Unlimited)

        """
        print_msg("[Google Play Music] [Situation search in "\
                  "Google Play Music] : '{0}'. " \
                  .format(arg.encode('utf-8')))
        try:
            situation_hits = self.__gmusic.search(arg)['situation_hits']

            if not len(situation_hits):
                # Do another search with an empty string
                situation_hits = self.__gmusic.search("")['situation_hits']
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            situation = next((hit for hit in situation_hits \
                              if 'best_result' in hit.keys()), None)

            num_tracks = 200
            if not situation and len(situation_hits):
                max_results = num_tracks / len(situation_hits)
                for hit in situation_hits:
                    situation = hit['situation']
                    print_nfo("[Google Play Music] [Situation] '{0} : {1}'." \
                              .format((hit['situation']['title']).encode('utf-8'),
                                      (hit['situation']['description']).encode('utf-8')))

                    self.__enqueue_station_unlimited(situation['title'], max_results, True)

            if not situation:
                raise KeyError

        except KeyError:
            raise KeyError("Situation not found : {0}".format(arg))

    def __enqueue_tracks(self, tracks):
        """ Add tracks to the playback queue

        """
        count = 0
        for track in tracks:
            if u'id' not in track.keys():
                track[u'id'] = track['nid']
            self.queue.append(track)
            count += 1
        return count

    def __update_playlists(self):
        """ Retrieve the user's playlists

        """
        plists = self.__gmusic.get_all_user_playlist_contents()
        for plist in plists:
            plist_name = plist['name']
            logging.info("playlist name : %s", to_ascii(plist_name))
            tracks = plist['tracks']
            tracks.sort(key=itemgetter('creationTimestamp'))
            self.playlists[plist_name] = list()
            for track in tracks:
                try:
                    song = self.song_map[track['trackId']]
                    self.playlists[plist_name].append(song)
                except IndexError:
                    pass

    def __update_playlists_unlimited(self):
        """ Retrieve shared playlists (Unlimited)

        """
        plists_subscribed_to = [p for p in self.__gmusic.get_all_playlists() \
                                if p.get('type') == 'SHARED']
        for plist in plists_subscribed_to:
            share_tok = plist['shareToken']
            playlist_items \
                = self.__gmusic.get_shared_playlist_contents(share_tok)
            plist_name = plist['name']
            logging.info("shared playlist name : %s", to_ascii(plist_name))
            self.playlists[plist_name] = list()
            for item in playlist_items:
                try:
                    song = item['track']
                    song['id'] = item['trackId']
                    self.playlists[plist_name].append(song)
                except IndexError:
                    pass

    def __gmusic_search(self, query, query_type, max_results=200, quiet=False):
        """ Search Google Play (Unlimited)

        """

        search_results = self.__gmusic.search(query, max_results)[query_type + '_hits']
        result = next((hit for hit in search_results \
                            if 'best_result' in hit.keys()), None)

        if not result and len(search_results):
            secondary_hit = None
            for hit in search_results:
                if not quiet:
                    print_nfo("[Google Play Music] [{0}] '{1}'." \
                              .format(query_type.capitalize(),
                                      (hit[query_type]['name']).encode('utf-8')))
                if query.lower() == \
                   to_ascii(hit[query_type]['name']).lower():
                    result = hit
                    break
                if query.lower() in \
                   to_ascii(hit[query_type]['name']).lower():
                    secondary_hit = hit
            if not result and secondary_hit:
                result = secondary_hit

        if not result and not len(search_results):
            # Do another search with an empty string
            search_results = self.__gmusic.search("")[query_type + '_hits']

        if not result and len(search_results):
            # Play some random result from the search results
            random.seed()
            result = random.choice(search_results)
            if not quiet:
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(query.encode('utf-8')))

        return result
class tizgmusicproxy(object):
    """A class for accessing a Google Music account to retrieve song URLs.
    """

    all_songs_album_title = "All Songs"
    thumbs_up_playlist_name = "Thumbs Up"

    def __init__(self, email, password, device_id):
        self.__api = Mobileclient()
        self.logged_in = False
        self.__device_id = device_id
        self.queue = list()
        self.queue_index = -1
        self.play_mode = 0
        self.now_playing_song = None

        attempts = 0
        while not self.logged_in and attempts < 3:
            self.logged_in = self.__api.login(email, password)
            attempts += 1

        self.playlists = CaseInsensitiveDict()
        self.library = CaseInsensitiveDict()

    def logout(self):
        self.__api.logout()

    def update_local_lib(self):
        songs = self.__api.get_all_songs()
        self.playlists[self.thumbs_up_playlist_name] = list()

        # Get main library
        song_map = dict()
        for song in songs:
            if "rating" in song and song["rating"] == "5":
                self.playlists[self.thumbs_up_playlist_name].append(song)

            song_id = song["id"]
            song_artist = song["artist"]
            song_album = song["album"]

            song_map[song_id] = song

            if song_artist == "":
                song_artist = "Unknown Artist"

            if song_album == "":
                song_album = "Unknown Album"

            if not (song_artist in self.library):
                self.library[song_artist] = dict()
                self.library[song_artist][self.all_songs_album_title] = list()

            if not (song_album in self.library[song_artist]):
                self.library[song_artist][song_album] = list()

            self.library[song_artist][song_album].append(song)
            self.library[song_artist][self.all_songs_album_title].append(song)

        # Sort albums by track number
        for artist in self.library.keys():
            logging.info("Artist : {0}".format(artist.encode("utf-8")))
            for album in self.library[artist].keys():
                logging.info("   Album : {0}".format(album.encode("utf-8")))
                if album == self.all_songs_album_title:
                    sorted_album = sorted(self.library[artist][album], key=lambda k: k["title"])
                else:
                    sorted_album = sorted(self.library[artist][album], key=lambda k: k.get("trackNumber", 0))
                self.library[artist][album] = sorted_album

        # Get all playlists
        plists = self.__api.get_all_user_playlist_contents()
        for plist in plists:
            plist_name = plist["name"]
            self.playlists[plist_name] = list()
            for track in plist["tracks"]:
                try:
                    song = song_map[track["trackId"]]
                    self.playlists[plist_name].append(song)
                except IndexError:
                    pass

    def current_song_title_and_artist(self):
        logging.info("current_song_title_and_artist")
        song = self.now_playing_song
        if song is not None:
            title = self.now_playing_song["title"]
            artist = self.now_playing_song["artist"]
            logging.info("Now playing {0} by {1}".format(title.encode("utf-8"), artist.encode("utf-8")))
            return artist.encode("utf-8"), title.encode("utf-8")
        else:
            return "", ""

    def current_song_album_and_duration(self):
        logging.info("current_song_album_and_duration")
        song = self.now_playing_song
        if song is not None:
            album = self.now_playing_song["album"]
            duration = self.now_playing_song["durationMillis"]
            logging.info("album {0} duration {1}".format(album.encode("utf-8"), duration.encode("utf-8")))
            return album.encode("utf-8"), int(duration)
        else:
            return "", 0

    def current_song_track_number_and_total_tracks(self):
        logging.info("current_song_track_number_and_total_tracks")
        song = self.now_playing_song
        if song is not None:
            track = self.now_playing_song["trackNumber"]
            total = self.now_playing_song["totalTrackCount"]
            logging.info("track number {0} total tracks {1}".format(track, total))
            return track, total
        else:
            logging.info("current_song_track_number_and_total_tracks : not found")
            return 0, 0

    def clear_queue(self):
        self.queue = list()
        self.queue_index = -1

    def enqueue_artist(self, arg):
        try:
            artist = self.library[arg]
            count = 0
            for album in artist:
                for song in artist[album]:
                    self.queue.append(song)
                    count += 1
            logging.info("Added {0} tracks by {1} to queue".format(count, arg))
        except KeyError:
            logging.info("Cannot find {0}".format(arg))
            raise

    def enqueue_album(self, arg):
        try:
            for artist in self.library:
                for album in self.library[artist]:
                    logging.info("enqueue album : {0} | {1}".format(artist.encode("utf-8"), album.encode("utf-8")))
                    if album.lower() == arg.lower():
                        count = 0
                        for song in self.library[artist][album]:
                            self.queue.append(song)
                            count += 1
                        logging.info(
                            "Added {0} tracks from {1} by "
                            "{2} to queue".format(count, album.encode("utf-8"), artist.encode("utf-8"))
                        )
        except KeyError:
            logging.info("Cannot find {0}".format(arg))
            raise

    def enqueue_playlist(self, arg):
        try:
            playlist = self.playlists[arg]
            count = 0
            for song in playlist:
                self.queue.append(song)
                count += 1
            logging.info("Added {0} tracks from {1} to queue".format(count, arg))
        except KeyError:
            logging.info("Cannot find {0}".format(arg))
            raise

    def next_url(self):
        logging.info("next_url")
        if len(self.queue):
            self.queue_index += 1
            if (self.queue_index < len(self.queue)) and (self.queue_index >= 0):
                next_song = self.queue[self.queue_index]
                return self.__get_song_url(next_song)
            else:
                self.queue_index = -1
                return self.next_url()
        else:
            return ""

    def prev_url(self):
        if len(self.queue):
            self.queue_index -= 1
            if (self.queue_index < len(self.queue)) and (self.queue_index >= 0):
                prev_song = self.queue[self.queue_index]
                return self.__get_song_url(prev_song)
            else:
                self.queue_index = len(self.queue)
                return self.prev_url()
        else:
            return ""

    def __get_song_url(self, song):
        song_url = self.__api.get_stream_url(song["id"], self.__device_id)
        try:
            self.now_playing_song = song
            return song_url
        except AttributeError:
            logging.info("Could not retrieve song url!")
            raise
Exemple #20
0
class BlinkSyncModule:
    """Class to initialize sync module."""
    def __init__(self, blink, network_name, network_id, camera_list):
        """
        Initialize Blink sync module.

        :param blink: Blink class instantiation
        """
        self.blink = blink
        self.network_id = network_id
        self.region_id = blink.auth.region_id
        self.name = network_name
        self.serial = None
        self.status = "offline"
        self.sync_id = None
        self.host = None
        self.summary = None
        self.network_info = None
        self.events = []
        self.cameras = CaseInsensitiveDict({})
        self.motion_interval = blink.motion_interval
        self.motion = {}
        self.last_record = {}
        self.camera_list = camera_list
        self.available = False

    @property
    def attributes(self):
        """Return sync attributes."""
        attr = {
            "name": self.name,
            "id": self.sync_id,
            "network_id": self.network_id,
            "serial": self.serial,
            "status": self.status,
            "region_id": self.region_id,
        }
        return attr

    @property
    def urls(self):
        """Return device urls."""
        return self.blink.urls

    @property
    def online(self):
        """Return boolean system online status."""
        try:
            return ONLINE[self.status]
        except KeyError:
            _LOGGER.error("Unknown sync module status %s", self.status)
            self.available = False
            return False

    @property
    def arm(self):
        """Return status of sync module: armed/disarmed."""
        try:
            return self.network_info["network"]["armed"]
        except (KeyError, TypeError):
            self.available = False
            return None

    @arm.setter
    def arm(self, value):
        """Arm or disarm camera."""
        if value:
            return api.request_system_arm(self.blink, self.network_id)
        return api.request_system_disarm(self.blink, self.network_id)

    def start(self):
        """Initialize the system."""
        response = self.sync_initialize()
        if not response:
            return False

        try:
            self.sync_id = self.summary["id"]
            self.serial = self.summary["serial"]
            self.status = self.summary["status"]
        except KeyError:
            _LOGGER.error("Could not extract some sync module info: %s",
                          response)

        is_ok = self.get_network_info()
        self.check_new_videos()

        if not is_ok or not self.update_cameras():
            return False
        self.available = True
        return True

    def sync_initialize(self):
        """Initialize a sync module."""
        response = api.request_syncmodule(self.blink, self.network_id)
        try:
            self.summary = response["syncmodule"]
            self.network_id = self.summary["network_id"]
        except (TypeError, KeyError):
            _LOGGER.error(
                "Could not retrieve sync module information with response: %s",
                response)
            return False
        return response

    def update_cameras(self, camera_type=BlinkCamera):
        """Update cameras from server."""
        try:
            for camera_config in self.camera_list:
                if "name" not in camera_config:
                    break
                blink_camera_type = camera_config.get("type", "")
                name = camera_config["name"]
                self.motion[name] = False
                owl_info = self.get_owl_info(name)
                if blink_camera_type == "mini":
                    camera_type = BlinkCameraMini
                self.cameras[name] = camera_type(self)
                camera_info = self.get_camera_info(camera_config["id"],
                                                   owl_info=owl_info)
                self.cameras[name].update(camera_info,
                                          force_cache=True,
                                          force=True)

        except KeyError:
            _LOGGER.error("Could not create camera instances for %s",
                          self.name)
            return False
        return True

    def get_owl_info(self, name):
        """Extract owl information."""
        try:
            for owl in self.blink.homescreen["owls"]:
                if owl["name"] == name:
                    return owl
        except KeyError:
            pass
        return None

    def get_events(self, **kwargs):
        """Retrieve events from server."""
        force = kwargs.pop("force", False)
        response = api.request_sync_events(self.blink,
                                           self.network_id,
                                           force=force)
        try:
            return response["event"]
        except (TypeError, KeyError):
            _LOGGER.error("Could not extract events: %s", response)
            return False

    def get_camera_info(self, camera_id, **kwargs):
        """Retrieve camera information."""
        owl = kwargs.get("owl_info", None)
        if owl is not None:
            return owl
        response = api.request_camera_info(self.blink, self.network_id,
                                           camera_id)
        try:
            return response["camera"][0]
        except (TypeError, KeyError):
            _LOGGER.error("Could not extract camera info: %s", response)
            return {}

    def get_network_info(self):
        """Retrieve network status."""
        self.network_info = api.request_network_update(self.blink,
                                                       self.network_id)
        try:
            if self.network_info["network"]["sync_module_error"]:
                raise KeyError
        except (TypeError, KeyError):
            self.available = False
            return False
        return True

    def refresh(self, force_cache=False):
        """Get all blink cameras and pulls their most recent status."""
        if not self.get_network_info():
            return
        self.check_new_videos()
        for camera_name in self.cameras.keys():
            camera_id = self.cameras[camera_name].camera_id
            camera_info = self.get_camera_info(
                camera_id, owl_info=self.get_owl_info(camera_name))
            self.cameras[camera_name].update(camera_info,
                                             force_cache=force_cache)
        self.available = True

    def check_new_videos(self):
        """Check if new videos since last refresh."""
        try:
            interval = self.blink.last_refresh - self.motion_interval * 60
        except TypeError:
            # This is the first start, so refresh hasn't happened yet.
            # No need to check for motion.
            return False

        resp = api.request_videos(self.blink, time=interval, page=1)

        for camera in self.cameras.keys():
            self.motion[camera] = False

        try:
            info = resp["media"]
        except (KeyError, TypeError):
            _LOGGER.warning("Could not check for motion. Response: %s", resp)
            return False

        for entry in info:
            try:
                name = entry["device_name"]
                clip = entry["media"]
                timestamp = entry["created_at"]
                if self.check_new_video_time(timestamp):
                    self.motion[name] = True and self.arm
                    self.last_record[name] = {"clip": clip, "time": timestamp}
            except KeyError:
                _LOGGER.debug("No new videos since last refresh.")

        return True

    def check_new_video_time(self, timestamp):
        """Check if video has timestamp since last refresh."""
        return time_to_seconds(timestamp) > self.blink.last_refresh
Exemple #21
0
    def _recommendation(self, venues_data: CaseInsensitiveDict,
                        user_pref: Dict) -> Dict:

        logging.info(
            f"Generating a recommendation report to advise which venues should the team members go."
        )

        places_to_avoid = list()

        for bad_food, _users in user_pref['bad_food_user_mappings'].items():
            for u in _users:
                for venue, available_food in venues_data.items():

                    # Assumption: if the venue offers anything else but what the user won't eat, then
                    # it is assumed that the user is comfortable in visiting that place. However,
                    # if the venue only offers the very food that the user won't eat then it is assumed that
                    # that venue should be avoided...

                    list_of_foods_offered_by_venue = list(
                        set(x.strip().lower() for x in available_food['food']))

                    if ([bad_food] == list_of_foods_offered_by_venue) or (
                            len(list_of_foods_offered_by_venue) == 0):
                        # [bad_food] == list_of_foods_offered_by_venue will return true only when the
                        # venue offers one type of dish and that happens to be the one that the user won't eat.

                        # len(list_of_foods_offered_by_venue) == 0 means that venue doesn't offer anything at all,
                        # hence it should be avoided.

                        places_to_avoid.append({
                            venue:
                            f'There is nothing for {u.split()[0]} to eat.'
                        })

        for _user, drinks in user_pref['user_drinks_mappings'].items():
            for _v_name, available_drinks in venues_data.items():

                set_of_drinks_offered_by_venue = set(
                    x.strip().lower() for x in available_drinks['drinks'])

                if len(
                        set(drinks).intersection(
                            set_of_drinks_offered_by_venue)) == 0:

                    # len(set(drinks).intersection(set_of_drinks_offered_by_venue)) == 0 will return True if
                    # the venue offers none of the drinks that are desired by the users; In either case
                    # this suggests that the team cannot go to that venue.

                    places_to_avoid.append({
                        _v_name:
                        f'There is nothing for {_user.split()[0]} to drink.'
                    })

        avoid_dict = self._merge_dicts(places_to_avoid, default_type=list)

        return {
            'places_to_visit': [
                v for v in venues_data.keys()
                if v not in [key for key in avoid_dict.keys()]
            ],
            'places_to_avoid': [{
                'name': key,
                'reason': value
            } for key, value in avoid_dict.items()]
        }
Exemple #22
0
    def make_request(self,
                     method,
                     bucket,
                     key=None,
                     subresource=None,
                     params=None,
                     data=None,
                     headers=None):

        # Remove params that are set to None
        if params is None:
            params = {}
        for k, v in params.copy().items():
            if v is None:
                params.pop(k)

        # Construct target url
        url = 'http://{}/{}'.format(self.hostname, bucket)
        url += '/{}'.format(key) if key is not None else '/'
        if subresource is not None:
            url += '?{}'.format(subresource)
        elif len(params) > 0:
            url += '?{}'.format(urllib.urlencode(params))

        # Make headers case insensitive
        if headers is None:
            headers = {}
        headers = CaseInsensitiveDict(headers)

        headers['Host'] = self.hostname

        if self.temporary_security_token is not None:
            headers['x-amz-security-token'] = self.temporary_security_token

        if data is not None:
            try:
                raw_md5 = utils.f_md5(data)
            except:
                m = hashlib.md5()
                m.update(data)
                raw_md5 = m.digest()
            md5 = b64encode(raw_md5)
            headers['Content-MD5'] = md5
        else:
            md5 = ''

        try:
            content_type = headers['Content-Type']
        except KeyError:
            content_type = ''

        date = formatdate(timeval=None, localtime=False, usegmt=True)
        headers['x-amz-date'] = date

        # Construct canonicalized amz headers string
        canonicalized_amz_headers = ''
        amz_keys = [k for k in list(headers.keys()) if k.startswith('x-amz-')]
        for k in sorted(amz_keys):
            v = headers[k].strip()
            k = k.lower()
            canonicalized_amz_headers += '{}:{}\n'.format(k, v)

        # Construct canonicalized resource string
        canonicalized_resource = '/' + bucket
        canonicalized_resource += '/' if key is None else '/{}'.format(key)
        if subresource is not None:
            canonicalized_resource += '?{}'.format(subresource)
        elif len(params) > 0:
            canonicalized_resource += '?{}'.format(urllib.urlencode(params))

        # Construct string to sign
        string_to_sign = method.upper() + '\n'
        string_to_sign += md5 + '\n'
        string_to_sign += content_type + '\n'
        string_to_sign += '\n'  # date is always set through x-amz-date
        string_to_sign += canonicalized_amz_headers + canonicalized_resource

        # Create signature
        h = hmac.new(self.secret_access_key, string_to_sign, hashlib.sha1)
        signature = b64encode(h.digest())

        # Set authorization header
        auth_head = 'AWS {}:{}'.format(self.access_key_id, signature)
        headers['Authorization'] = auth_head

        # Prepare Request
        req = Request(method, url, data=data, headers=headers).prepare()

        # Log request data.
        # Prepare request beforehand so requests-altered headers show.
        # Combine into a single message so we don't have to bother with
        # locking to make lines appear together.
        log_message = '{} {}\n'.format(method, url)
        log_message += 'string to sign: {}\n'.format(repr(string_to_sign))
        log_message += 'headers:'
        for k in sorted(req.headers.keys()):
            log_message += '\n {}: {}'.format(k, req.headers[k])
        log.debug(log_message)

        # Send request
        resp = Session().send(req)

        # Update stats, log response data.
        self.stats[method.upper()] += 1
        log.debug('response: {} ({} {})'.format(resp.status_code, method, url))

        # Handle errors
        if resp.status_code / 100 != 2:
            soup = BeautifulSoup(resp.text)
            error = soup.find('error')

            log_message = "S3 replied with non 2xx response code!!!!\n"
            log_message += '  request: {} {}\n'.format(method, url)
            for c in error.children:
                error_name = c.name
                error_message = c.text.encode('unicode_escape')
                log_message += '  {}: {}\n'.format(error_name, error_message)
            log.debug(log_message)

            code = error.find('code').text
            message = error.find('message').text
            raise S3ResponseError(code, message, resp)

        return resp
class ReminderManager():
    def __init__(self, bot, google_creds, relay_map):
        self.bot = bot
        self.google_creds = google_creds
        self.relay_map = CaseInsensitiveDict(relay_map)
        self.tasks = []

    async def initialize(self):
        logging.info("Initializing google calendar reminders...")
        # For each google calendar, create a new async task that will poll for upcoming events
        for calendar_label in self.relay_map.keys():
            config = self.relay_map[calendar_label]

            calendar_id = config['calendar_id']
            channel_ids = config['channels']
            # This is a list of integers representing how many minutes prior to the event do we want a reminder
            when_to_notify = config['when']
            ping = config['ping']

            channels = []
            for channel_id in channel_ids:
                channel = discord.utils.get(self.bot.get_all_channels(),
                                            id=int(channel_id))
                if not channel:
                    logging.warning(
                        f"\tBot does not have access to channel with ID {channel_id}!"
                    )
                else:
                    channels.append(channel)
            logging.info(
                f"\tWatching for events from Google Calendar {calendar_label} with id {calendar_id}..."
            )
            self.tasks.append(
                asyncio.create_task(
                    self.poll_calendar_events(calendar_id, channels,
                                              when_to_notify, ping)))
        logging.info("Done.")

    async def auth(self):
        # Grabs an authenticated endpoint for pulling calendar data
        credentials = service_account.Credentials.from_service_account_file(
            self.google_creds, scopes=SCOPES)
        return build('calendar',
                     'v3',
                     credentials=credentials,
                     cache_discovery=False)

    async def poll_calendar_events(self, calendar_id, channels, when_to_notify,
                                   ping):
        # This cache is local to only this running coroutine
        # We only want to notify an event for a particular "minutes until event" trigger one time
        cache = {}
        logging.info('Spawned a poll watcher. Trump would be proud.')
        for look_ahead in when_to_notify:
            cache[look_ahead] = set()

        while True:
            # Grab a timezone-aware timestamp for "now", in UTC time
            right_now = datetime.now(timezone.utc)
            for look_ahead in when_to_notify:
                # Based on the "minutes until event" value, create a "look ahead" window of a minute
                # Ex: imagine a value of "2" for "2 minutes before event, notify"
                #
                #         now                              look ahead window
                #      (12:00:30)                          |---------------|
                #   |--------------|---------------|--------------|--------------|
                # 12:00          12:01           12:02          12:03          12:04
                #
                # TODO: This isn't ideal. This window of one minute means that we're notifying
                # up to a minute too soon, depending on where "now" falls on the seconds clock.
                start_after = right_now + timedelta(minutes=look_ahead)
                start_before = start_after + timedelta(minutes=1)
                for channel in channels:
                    newline = "\n"
                    await self.get_events_in_window(
                        calendar_id, start_after, start_before, channel,
                        look_ahead, cache[look_ahead],
                        f'📅  🐱 💬  {"@here " if ping else ""}' +
                        f'Events are starting {"in " + str(look_ahead) + " minutes" if look_ahead > 0 else "now"}!'
                    )
            await asyncio.sleep(CALENDAR_POLL_INTERVAL)

    async def get_events_in_window(self, calendar_id, start_after,
                                   start_before, channel, look_ahead, cache,
                                   prompt):
        try:
            # Construct the query to Google Calendar. We're only able to provide a cutoff for starting after a date.
            # So we'll validate the "starting before" the other end of the window later.
            service = await self.auth()
            result = service.events().list(
                calendarId=calendar_id,
                singleEvents=True,
                orderBy='startTime',
                timeMin=f'{start_after.isoformat(timespec="seconds")}',
                maxResults=5).execute()

            # Edge case - no data comes back
            if 'items' not in result:
                return

            # Filter out anything that's not an event
            future_events = [
                item for item in result['items']
                if 'kind' in item and item['kind'] == 'calendar#event'
            ]

            # Filter out anything that's already in the cache
            uncached_future_events = [
                item for item in future_events if item['id'] not in cache
            ]
            if not uncached_future_events:
                return

            # Some events may only have a date. This just converts those dates to datetime objects (midnight on date).
            await self._change_events_start_date_to_datetime(
                uncached_future_events)

            # Filter out anything that's not actually in the "window"
            to_notify = []
            for future_event in uncached_future_events:
                start = future_event['start']
                when = datetime.fromisoformat(
                    future_event['start']['dateTime'])
                if when >= start_after and when < start_before:
                    to_notify.append(future_event)
            if not to_notify:
                return

            # Construct the message for the notification.
            msg = f"{prompt}\n"
            for future_event in to_notify:
                if future_event['id'] in cache:
                    continue

                cache.add(future_event['id'])
                event_as_str = await self._render_event(future_event)
                msg += f'```{event_as_str}```' + "\n"
                logging.info(
                    f"  Event {future_event['id']} reminder being sent from calendar {calendar_id} to channel {channel.id}."
                )
            await channel.send(msg)
        except Exception as e:
            logging.exception(
                f'Exception thrown while attempting to check events on calendar {calendar_id} for channel {channel.id}'
            )

    async def get_upcoming_events(self, channel, calendar_name=None):
        if not channel:
            return

        available_calendars = '\n'.join(self.relay_map.keys())
        if not calendar_name:
            return await channel.send(
                f'😾  Which one? Try using `!event <calendar>` with one of these.\n```{available_calendars}```'
            )

        if calendar_name not in self.relay_map:
            return await channel.send(
                f'🙀  I only know of these calendars.\n```{available_calendars}```'
            )

        calendar_id = self.relay_map[calendar_name]['calendar_id']
        right_now = right_now = datetime.now(timezone.utc)

        # Calculate the number of days to add to get to the last day of next month.
        # There's probably a better way to do this, because this really stinks.
        next_month_year = right_now.year
        next_month = right_now.month + 1
        if next_month == 13:
            next_month_year += 1
            next_month = 1
        days_in_next_month = monthrange(next_month_year, next_month)[1]
        days_in_this_month = monthrange(right_now.year, right_now.month)[1]
        total_days_add = (days_in_this_month -
                          right_now.day) + days_in_next_month
        last_day_of_next_month = right_now + timedelta(days=+total_days_add)

        # Okay, now we have SOME time ON the last day. Let's cut that time off, and
        # force it to be the last second of the day.
        last_day_of_next_month.strftime('%Y-%m-%d') + 'T23:59:59Z'

        service = await self.auth()
        result = service.events().list(
            calendarId=calendar_id,
            singleEvents=True,
            orderBy='startTime',
            timeMin=f'{right_now.isoformat(timespec="seconds")}',
            timeMax=f'{last_day_of_next_month.strftime("%Y-%m-%d")}T23:59:59Z',
            maxResults=20).execute()
        if not 'items' in result:
            return

        future_events = [
            item for item in result['items']
            if 'kind' in item and item['kind'] == 'calendar#event'
        ]
        if not future_events:
            return

        # Convert all events with only dates to have datetimes starting at midnight
        await self._change_events_start_date_to_datetime(future_events)
        future_events = sorted(
            future_events,
            key=lambda item: datetime.fromisoformat(item['start']['dateTime']))

        msg = "📅  🐱 💬  There are some meetings and events coming up...\n"
        for future_event in future_events:
            event_as_str = await self._render_event(future_event)

            if len(msg + event_as_str) > 1900:
                break

            msg += f'```{event_as_str}```' + "\n"
        await channel.send(msg)

    async def _change_events_start_date_to_datetime(self, future_events):
        for future_event in future_events:
            start = future_event['start']
            if 'date' in start:
                new_start = {'dateTime': start['date'] + 'T00:00:00-00:00'}
                future_event['start'] = new_start

    async def _render_event(self, future_event):
        start = future_event['start']
        when = datetime.fromisoformat(
            future_event['start']['dateTime']).astimezone(EASTERN_TIMEZONE)
        when_str = when.strftime("%A, %d. %B %Y %I:%M%p %Z").replace(
            '12:00AM EST', '')
        location = future_event[
            'location'] if 'location' in future_event else ''
        description = future_event[
            'description'] if 'description' in future_event else ''

        # sanitize html nonsense from the description
        if BeautifulSoup(description, 'html.parser').find():
            description = description.replace('<br>', '\n').replace(
                '<wbr>', '').replace('&nbsp;', '')
            description = BeautifulSoup(description, "lxml").text

        newline = "\n"
        return f'{future_event["summary"]}    {when_str}{newline}'\
            f'{newline + location if location else ""}' \
            f'{newline + description if description else ""}'.strip()
Exemple #24
0
    def reload_config(self, config, sighup=False):
        self._superuser = config['authentication'].get('superuser', {})
        server_parameters = self.get_server_parameters(config)

        conf_changed = hba_changed = ident_changed = local_connection_address_changed = pending_restart = False
        if self._postgresql.state == 'running':
            changes = CaseInsensitiveDict(
                {p: v
                 for p, v in server_parameters.items() if '.' not in p})
            changes.update({
                p: None
                for p in self._server_parameters.keys()
                if not ('.' in p or p in changes)
            })
            if changes:
                if 'wal_buffers' in changes:  # we need to calculate the default value of wal_buffers
                    undef = [
                        p for p in ('shared_buffers', 'wal_segment_size',
                                    'wal_block_size') if p not in changes
                    ]
                    changes.update({p: None for p in undef})
                # XXX: query can raise an exception
                old_values = {
                    r[0]: r
                    for r in self._postgresql.query((
                        'SELECT name, setting, unit, vartype, context ' +
                        'FROM pg_catalog.pg_settings ' +
                        ' WHERE pg_catalog.lower(name) = ANY(%s)'
                    ), [k.lower() for k in changes.keys()])
                }
                if 'wal_buffers' in changes:
                    self._handle_wal_buffers(old_values, changes)
                    for p in undef:
                        del changes[p]

                for r in old_values.values():
                    if r[4] != 'internal' and r[0] in changes:
                        new_value = changes.pop(r[0])
                        if new_value is None or not compare_values(
                                r[3], r[2], r[1], new_value):
                            conf_changed = True
                            if r[4] == 'postmaster':
                                pending_restart = True
                                logger.info(
                                    'Changed %s from %s to %s (restart might be required)',
                                    r[0], r[1], new_value)
                                if config.get('use_unix_socket') and r[0] == 'unix_socket_directories'\
                                        or r[0] in ('listen_addresses', 'port'):
                                    local_connection_address_changed = True
                            else:
                                logger.info('Changed %s from %s to %s', r[0],
                                            r[1], new_value)
                for param in changes:
                    if param in server_parameters:
                        logger.warning(
                            'Removing invalid parameter `%s` from postgresql.parameters',
                            param)
                        server_parameters.pop(param)

            # Check that user-defined-paramters have changed (parameters with period in name)
            for p, v in server_parameters.items():
                if '.' in p and (p not in self._server_parameters
                                 or str(v) != str(self._server_parameters[p])):
                    logger.info('Changed %s from %s to %s', p,
                                self._server_parameters.get(p), v)
                    conf_changed = True
            for p, v in self._server_parameters.items():
                if '.' in p and (p not in server_parameters
                                 or str(v) != str(server_parameters[p])):
                    logger.info('Changed %s from %s to %s', p, v,
                                server_parameters.get(p))
                    conf_changed = True

            if not server_parameters.get('hba_file') and config.get('pg_hba'):
                hba_changed = self._config.get('pg_hba',
                                               []) != config['pg_hba']

            if not server_parameters.get('ident_file') and config.get(
                    'pg_ident'):
                ident_changed = self._config.get('pg_ident',
                                                 []) != config['pg_ident']

        self._config = config
        self._postgresql.set_pending_restart(pending_restart)
        self._server_parameters = server_parameters
        self._adjust_recovery_parameters()
        self._krbsrvname = config.get('krbsrvname')

        # for not so obvious connection attempts that may happen outside of pyscopg2
        if self._krbsrvname:
            os.environ['PGKRBSRVNAME'] = self._krbsrvname

        if not local_connection_address_changed:
            self.resolve_connection_addresses()

        if conf_changed:
            self.write_postgresql_conf()

        if hba_changed:
            self.replace_pg_hba()

        if ident_changed:
            self.replace_pg_ident()

        if sighup or conf_changed or hba_changed or ident_changed:
            logger.info('Reloading PostgreSQL configuration.')
            self._postgresql.reload()
            if self._postgresql.major_version >= 90500:
                time.sleep(1)
                try:
                    pending_restart = self._postgresql.query(
                        'SELECT COUNT(*) FROM pg_catalog.pg_settings'
                        ' WHERE pending_restart').fetchone()[0] > 0
                    self._postgresql.set_pending_restart(pending_restart)
                except Exception as e:
                    logger.warning('Exception %r when running query', e)
        else:
            logger.info(
                'No PostgreSQL configuration items changed, nothing to reload.'
            )
Exemple #25
0
    def run(self, tmp=None, task_vars=None):
        ''' handler for template operations '''

        self.nsbl_env = os.environ.get("NSBL_ENVIRONMENT", False) == "true"

        if task_vars is None:
            task_vars = dict()

        result = super(ActionModule, self).run(tmp, task_vars)
        format = {
            "child_marker": "packages",
            "default_leaf": "vars",
            "default_leaf_key": "name",
            "key_move_map": {
                '*': "vars"
            }
        }
        chain = [frkl.FrklProcessor(format)]

        frkl_obj = frkl.Frkl(self._task.args["packages"], chain)

        package = frkl_obj.process()
        if len(package) == 0:
            raise Exception("No packages provided for package: {}".format(
                self._task.args["packages"]))
        if len(package) != 1:
            raise Exception(
                "For some reason more than one package provided, this shouldn't happen: {}"
                .format(package))

        package = package[0]

        if "pkg_mgr" not in package[VARS_KEY].keys():
            pkg_mgr = self._task.args.get('pkg_mgr', 'auto')
        else:
            pkg_mgr = package[VARS_KEY]["pkg_mgr"]

        if pkg_mgr == 'auto':
            try:
                if self._task.delegate_to:
                    pkg_mgr = self._templar.template(
                        "{{hostvars['%s']['ansible_facts']['ansible_pkg_mgr']}}"
                        % self._task.delegate_to)
                else:
                    pkg_mgr = self._templar.template(
                        '{{ansible_facts["ansible_pkg_mgr"]}}')
            except Exception as e:
                pass  # could not get it from template!

        auto = pkg_mgr == 'auto'

        facts = self._execute_module(module_name='setup',
                                     module_args=dict(gather_subset='!all'),
                                     task_vars=task_vars)
        if auto:
            pkg_mgr = facts['ansible_facts'].get('ansible_pkg_mgr', None)
        os_family = facts['ansible_facts'].get('ansible_os_family', None)
        distribution = facts['ansible_facts'].get('ansible_distribution', None)
        distribution_major_version = facts['ansible_facts'].get(
            'ansible_distribution_major_version', None)
        distribution_version = facts['ansible_facts'].get(
            'ansible_distribution_version', None)
        distribution_release = facts['ansible_facts'].get(
            'ansible_distribution_release', None)
        # figure out actual package name
        if distribution_version:
            full_version_string = "{}-{}".format(distribution,
                                                 distribution_version).lower()
        else:
            full_version_string = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

        if distribution_release:
            full_release_string = "{}-{}".format(distribution,
                                                 distribution_release).lower()
        else:
            full_release_string = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

        if distribution_major_version:
            distribution_major_string = "{}-{}".format(
                distribution, distribution_major_version).lower()
        else:
            distribution_major_string = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

        distribution_string = distribution.lower()
        os_string = os_family.lower()

        if pkg_mgr == 'unknown' and os_family == "Darwin":
            pkg_mgr = "homebrew"

        if pkg_mgr in ['auto', 'unknown']:
            result['failed'] = True
            result[
                'msg'] = 'Could not detect which package manager to use. Try gathering facts or setting the "use" option.'
            return result

        if pkg_mgr not in self._shared_loader_obj.module_loader:
            result['failed'] = True
            result[
                'msg'] = "Could not find an ansible module for package manager '{}'.".format(
                    pkg_mgr)
            return result

        # calculate package name, just in case
        pkg_dict = CaseInsensitiveDict(package[VARS_KEY].get("pkgs"))
        if pkg_mgr.lower() in (name.lower() for name in pkg_dict.keys()):
            calculated_package_pkg_mgr = pkg_dict[pkg_mgr.lower()]
        elif 'other' in (name.lower() for name in pkg_dict.keys()):
            calculated_package_pkg_mgr = pkg_dict['other']
        else:
            calculated_package_pkg_mgr = None

        if full_version_string in (name.lower() for name in pkg_dict.keys()):
            calculated_package_platform = pkg_dict[full_version_string]
        elif full_release_string in (name.lower() for name in pkg_dict.keys()):
            calculated_package_platform = pkg_dict[full_release_string]
        elif distribution_major_string in (name.lower()
                                           for name in pkg_dict.keys()):
            calculated_package_platform = pkg_dict[distribution_major_string]
        elif distribution_string in (name.lower() for name in pkg_dict.keys()):
            calculated_package_platform = pkg_dict[distribution_string]
        elif os_string in (name.lower() for name in pkg_dict.keys()):
            calculated_package_platform = pkg_dict[os_string]
        elif 'other' in (name.lower() for name in pkg_dict.keys()):
            calculated_package_platform = pkg_dict['other']
        else:
            calculated_package_platform = None

            # if calculated_package_platform in ['ignore', 'omit'] or calculated_package_pkg_mgr in ['ignore', 'omit']:
            # result['msg'] = "Ignoring package {}".format(package[VARS_KEY]["name"])
            # result['skipped'] = True
            # return result

        if not auto or not calculated_package_platform:
            calculated_package = calculated_package_pkg_mgr
        else:
            calculated_package = calculated_package_platform

        if calculated_package in ['ignore', 'omit']:
            result['msg'] = "Ignoring package {}".format(
                package[VARS_KEY]["name"])
            result['skipped'] = True
            return result

        module_result = self.execute_package_module(package,
                                                    calculated_package, auto,
                                                    pkg_mgr, task_vars, result)

        if module_result:
            result.update(module_result)

        return result
Exemple #26
0
class tizgmusicproxy(object):
    """A class for logging into a Google Play Music account and retrieving song
    URLs.

    """

    all_songs_album_title = "All Songs"
    thumbs_up_playlist_name = "Thumbs Up"

    # pylint: disable=too-many-instance-attributes,too-many-public-methods
    def __init__(self, email, password, device_id):
        self.__gmusic = Mobileclient()
        self.__email = email
        self.__device_id = device_id
        self.logged_in = False
        self.queue = list()
        self.queue_index = -1
        self.play_queue_order = list()
        self.play_modes = TizEnumeration(["NORMAL", "SHUFFLE"])
        self.current_play_mode = self.play_modes.NORMAL
        self.now_playing_song = None

        userdir = os.path.expanduser('~')
        tizconfig = os.path.join(userdir,
                                 ".config/tizonia/." + email + ".auth_token")
        auth_token = ""
        if os.path.isfile(tizconfig):
            with open(tizconfig, "r") as f:
                auth_token = pickle.load(f)
                if auth_token:
                    # 'Keep track of the auth token' workaround. See:
                    # https://github.com/diraimondo/gmusicproxy/issues/34#issuecomment-147359198
                    print_msg("[Google Play Music] [Authenticating] : " \
                              "'with cached auth token'")
                    self.__gmusic.android_id = device_id
                    self.__gmusic.session._authtoken = auth_token
                    self.__gmusic.session.is_authenticated = True
                    try:
                        self.__gmusic.get_registered_devices()
                    except CallFailure:
                        # The token has expired. Reset the client object
                        print_wrn("[Google Play Music] [Authenticating] : " \
                                  "'auth token expired'")
                        self.__gmusic = Mobileclient()
                        auth_token = ""

        if not auth_token:
            attempts = 0
            print_nfo("[Google Play Music] [Authenticating] : " \
                      "'with user credentials'")
            while not self.logged_in and attempts < 3:
                self.logged_in = self.__gmusic.login(email, password,
                                                     device_id)
                attempts += 1

            with open(tizconfig, "a+") as f:
                f.truncate()
                pickle.dump(self.__gmusic.session._authtoken, f)

        self.library = CaseInsensitiveDict()
        self.song_map = CaseInsensitiveDict()
        self.playlists = CaseInsensitiveDict()
        self.stations = CaseInsensitiveDict()

    def logout(self):
        """ Reset the session to an unauthenticated, default state.

        """
        self.__gmusic.logout()

    def set_play_mode(self, mode):
        """ Set the playback mode.

        :param mode: curren tvalid values are "NORMAL" and "SHUFFLE"

        """
        self.current_play_mode = getattr(self.play_modes, mode)
        self.__update_play_queue_order()

    def current_song_title_and_artist(self):
        """ Retrieve the current track's title and artist name.

        """
        logging.info("current_song_title_and_artist")
        song = self.now_playing_song
        if song:
            title = to_ascii(self.now_playing_song.get('title'))
            artist = to_ascii(self.now_playing_song.get('artist'))
            logging.info("Now playing %s by %s", title, artist)
            return artist, title
        else:
            return '', ''

    def current_song_album_and_duration(self):
        """ Retrieve the current track's album and duration.

        """
        logging.info("current_song_album_and_duration")
        song = self.now_playing_song
        if song:
            album = to_ascii(self.now_playing_song.get('album'))
            duration = to_ascii \
                       (self.now_playing_song.get('durationMillis'))
            logging.info("album %s duration %s", album, duration)
            return album, int(duration)
        else:
            return '', 0

    def current_track_and_album_total(self):
        """Return the current track number and the total number of tracks in the
        album, if known.

        """
        logging.info("current_track_and_album_total")
        song = self.now_playing_song
        track = 0
        total = 0
        if song:
            try:
                track = self.now_playing_song['trackNumber']
                total = self.now_playing_song['totalTrackCount']
                logging.info("track number %s total tracks %s", track, total)
            except KeyError:
                logging.info("trackNumber or totalTrackCount : not found")
        else:
            logging.info("current_song_track_number_"
                         "and_total_tracks : not found")
        return track, total

    def current_song_year(self):
        """ Return the current track's year of publication.

        """
        logging.info("current_song_year")
        song = self.now_playing_song
        year = 0
        if song:
            try:
                year = song['year']
                logging.info("track year %s", year)
            except KeyError:
                logging.info("year : not found")
        else:
            logging.info("current_song_year : not found")
        return year

    def clear_queue(self):
        """ Clears the playback queue.

        """
        self.queue = list()
        self.queue_index = -1

    def enqueue_tracks(self, arg):
        """ Search the user's library for tracks and add
        them to the playback queue.

        :param arg: a track search term
        """
        try:
            songs = self.__gmusic.get_all_songs()

            track_hits = list()
            for song in songs:
                song_title = song['title']
                if arg.lower() in song_title.lower():
                    track_hits.append(song)
                    print_nfo("[Google Play Music] [Track] '{0}'." \
                              .format(to_ascii(song_title)))

            if not len(track_hits):
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))
                random.seed()
                track_hits = random.sample(songs, MAX_TRACKS)
                for hit in track_hits:
                    song_title = hit['title']
                    print_nfo("[Google Play Music] [Track] '{0}'." \
                              .format(to_ascii(song_title)))

            if not len(track_hits):
                raise KeyError

            tracks_added = self.__enqueue_tracks(track_hits)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)

            self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Track not found : {0}".format(arg))

    def enqueue_artist(self, arg):
        """ Search the user's library for tracks from the given artist and add
        them to the playback queue.

        :param arg: an artist
        """
        try:
            self.__update_local_library()
            artist = None
            artist_dict = None
            if arg not in self.library.keys():
                for name, art in self.library.iteritems():
                    if arg.lower() in name.lower():
                        artist = name
                        artist_dict = art
                        if arg.lower() != name.lower():
                            print_wrn("[Google Play Music] '{0}' not found. " \
                                      "Playing '{1}' instead." \
                                      .format(arg.encode('utf-8'), \
                                              name.encode('utf-8')))
                        break
                if not artist:
                    # Play some random artist from the library
                    random.seed()
                    artist = random.choice(self.library.keys())
                    artist_dict = self.library[artist]
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))
            else:
                artist = arg
                artist_dict = self.library[arg]
            tracks_added = 0
            for album in artist_dict:
                tracks_added += self.__enqueue_tracks(artist_dict[album])
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(artist)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Artist not found : {0}".format(arg))

    def enqueue_album(self, arg):
        """ Search the user's library for albums with a given name and add
        them to the playback queue.

        """
        try:
            self.__update_local_library()
            album = None
            artist = None
            tentative_album = None
            tentative_artist = None
            for library_artist in self.library:
                for artist_album in self.library[library_artist]:
                    print_nfo("[Google Play Music] [Album] '{0}'." \
                              .format(to_ascii(artist_album)))
                    if not album:
                        if arg.lower() == artist_album.lower():
                            album = artist_album
                            artist = library_artist
                            break
                    if not tentative_album:
                        if arg.lower() in artist_album.lower():
                            tentative_album = artist_album
                            tentative_artist = library_artist
                if album:
                    break

            if not album and tentative_album:
                album = tentative_album
                artist = tentative_artist
                print_wrn("[Google Play Music] '{0}' not found. " \
                          "Playing '{1}' instead." \
                          .format(arg.encode('utf-8'), \
                          album.encode('utf-8')))
            if not album:
                # Play some random album from the library
                random.seed()
                artist = random.choice(self.library.keys())
                album = random.choice(self.library[artist].keys())
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            if not album:
                raise KeyError("Album not found : {0}".format(arg))

            self.__enqueue_tracks(self.library[artist][album])
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(album)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Album not found : {0}".format(arg))

    def enqueue_playlist(self, arg):
        """Search the user's library for playlists with a given name
        and add the tracks of the first match to the playback queue.

        Requires Unlimited subscription.

        """
        try:
            self.__update_local_library()
            self.__update_playlists()
            self.__update_playlists_unlimited()
            playlist = None
            playlist_name = None
            for name, plist in self.playlists.items():
                print_nfo("[Google Play Music] [Playlist] '{0}'." \
                          .format(to_ascii(name)))
            if arg not in self.playlists.keys():
                for name, plist in self.playlists.iteritems():
                    if arg.lower() in name.lower():
                        playlist = plist
                        playlist_name = name
                        if arg.lower() != name.lower():
                            print_wrn("[Google Play Music] '{0}' not found. " \
                                      "Playing '{1}' instead." \
                                      .format(arg.encode('utf-8'), \
                                              to_ascii(name)))
                            break
            else:
                playlist_name = arg
                playlist = self.playlists[arg]

            random.seed()
            x = 0
            while (not playlist or not len(playlist)) and x < 3:
                x += 1
                # Play some random playlist from the library
                playlist_name = random.choice(self.playlists.keys())
                playlist = self.playlists[playlist_name]
                print_wrn("[Google Play Music] '{0}' not found or found empty. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            if not len(playlist):
                raise KeyError

            self.__enqueue_tracks(playlist)
            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(playlist_name)))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError(
                "Playlist not found or found empty : {0}".format(arg))

    def enqueue_podcast(self, arg):
        """Search Google Play Music for a podcast series and add its tracks to the
        playback queue ().

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving podcasts] : '{0}'. " \
                  .format(self.__email))

        try:

            self.__enqueue_podcast(arg)

            if not len(self.queue):
                raise KeyError

            logging.info("Added %d episodes from '%s' to queue", \
                         len(self.queue), arg)
            self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Podcast not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_station_unlimited(self, arg):
        """Search the user's library for a station with a given name
        and add its tracks to the playback queue.

        Requires Unlimited subscription.

        """
        try:
            # First try to find a suitable station in the user's library
            self.__enqueue_user_station_unlimited(arg)

            if not len(self.queue):
                # If no suitable station is found in the user's library, then
                # search google play unlimited for a potential match.
                self.__enqueue_station_unlimited(arg)

            if not len(self.queue):
                raise KeyError

        except KeyError:
            raise KeyError("Station not found : {0}".format(arg))

    def enqueue_genre_unlimited(self, arg):
        """Search Unlimited for a genre with a given name and add its
        tracks to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving genres] : '{0}'. " \
                  .format(self.__email))

        try:
            all_genres = list()
            root_genres = self.__gmusic.get_genres()
            second_tier_genres = list()
            for root_genre in root_genres:
                second_tier_genres += self.__gmusic.get_genres(
                    root_genre['id'])
            all_genres += root_genres
            all_genres += second_tier_genres
            for genre in all_genres:
                print_nfo("[Google Play Music] [Genre] '{0}'." \
                          .format(to_ascii(genre['name'])))
            genre = dict()
            if arg not in all_genres:
                genre = next((g for g in all_genres \
                              if arg.lower() in to_ascii(g['name']).lower()), \
                             None)

            tracks_added = 0
            while not tracks_added:
                if not genre and len(all_genres):
                    # Play some random genre from the search results
                    random.seed()
                    genre = random.choice(all_genres)
                    print_wrn("[Google Play Music] '{0}' not found. "\
                              "Feeling lucky?." \
                              .format(arg.encode('utf-8')))

                genre_name = genre['name']
                genre_id = genre['id']
                station_id = self.__gmusic.create_station(genre_name, \
                                                          None, None, None, genre_id)
                num_tracks = MAX_TRACKS
                tracks = self.__gmusic.get_station_tracks(
                    station_id, num_tracks)
                tracks_added = self.__enqueue_tracks(tracks)
                logging.info("Added %d tracks from %s to queue", tracks_added,
                             genre_name)
                if not tracks_added:
                    # This will produce another iteration in the loop
                    genre = None

            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format(to_ascii(genre['name'])))
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Genre not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_situation_unlimited(self, arg):
        """Search Unlimited for a situation with a given name and add its
        tracks to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving situations] : '{0}'. " \
                  .format(self.__email))

        try:

            self.__enqueue_situation_unlimited(arg)

            if not len(self.queue):
                raise KeyError

            logging.info("Added %d tracks from %s to queue", \
                         len(self.queue), arg)

        except KeyError:
            raise KeyError("Situation not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_artist_unlimited(self, arg):
        """Search Unlimited for an artist and add the artist's 200 top tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        try:
            artist = self.__gmusic_search(arg, 'artist')

            include_albums = False
            max_top_tracks = MAX_TRACKS
            max_rel_artist = 0
            artist_tracks = dict()
            if artist:
                artist_tracks = self.__gmusic.get_artist_info \
                                (artist['artist']['artistId'],
                                 include_albums, max_top_tracks,
                                 max_rel_artist)['topTracks']

            if not artist_tracks:
                raise KeyError

            for track in artist_tracks:
                song_title = track['title']
                print_nfo("[Google Play Music] [Track] '{0}'." \
                          .format(to_ascii(song_title)))

            tracks_added = self.__enqueue_tracks(artist_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Artist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_album_unlimited(self, arg):
        """Search Unlimited for an album and add its tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        try:
            album = self.__gmusic_search(arg, 'album')
            album_tracks = dict()
            if album:
                album_tracks = self.__gmusic.get_album_info \
                               (album['album']['albumId'])['tracks']
            if not album_tracks:
                raise KeyError

            print_wrn("[Google Play Music] Playing '{0}'." \
                      .format((album['album']['name']).encode('utf-8')))

            tracks_added = self.__enqueue_tracks(album_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Album not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_tracks_unlimited(self, arg):
        """ Search Unlimited for a track name and add all the matching tracks
        to the playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving library] : '{0}'. " \
                  .format(self.__email))

        try:
            max_results = MAX_TRACKS
            track_hits = self.__gmusic.search(arg, max_results)['song_hits']
            if not len(track_hits):
                # Do another search with an empty string
                track_hits = self.__gmusic.search("", max_results)['song_hits']
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            tracks = list()
            for hit in track_hits:
                tracks.append(hit['track'])
            tracks_added = self.__enqueue_tracks(tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()
        except KeyError:
            raise KeyError("Playlist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_playlist_unlimited(self, arg):
        """Search Unlimited for a playlist name and add all its tracks to the
        playback queue.

        Requires Unlimited subscription.

        """
        print_msg("[Google Play Music] [Retrieving playlists] : '{0}'. " \
                  .format(self.__email))

        try:
            playlist_tracks = list()

            playlist_hits = self.__gmusic_search(arg, 'playlist')
            if playlist_hits:
                playlist = playlist_hits['playlist']
                playlist_contents = self.__gmusic.get_shared_playlist_contents(
                    playlist['shareToken'])
            else:
                raise KeyError

            print_nfo("[Google Play Music] [Playlist] '{}'." \
                      .format(playlist['name']).encode('utf-8'))

            for item in playlist_contents:
                print_nfo("[Google Play Music] [Playlist Track] '{} by {} (Album: {}, {})'." \
                          .format((item['track']['title']).encode('utf-8'),
                                  (item['track']['artist']).encode('utf-8'),
                                  (item['track']['album']).encode('utf-8'),
                                  (item['track']['year'])))
                track = item['track']
                playlist_tracks.append(track)

            if not playlist_tracks:
                raise KeyError

            tracks_added = self.__enqueue_tracks(playlist_tracks)
            logging.info("Added %d tracks from %s to queue", \
                         tracks_added, arg)
            self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Playlist not found : {0}".format(arg))
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def enqueue_promoted_tracks_unlimited(self):
        """ Retrieve the url of the next track in the playback queue.

        """
        try:
            tracks = self.__gmusic.get_promoted_songs()
            count = 0
            for track in tracks:
                store_track = self.__gmusic.get_track_info(track['storeId'])
                if u'id' not in store_track.keys():
                    store_track[u'id'] = store_track['storeId']
                self.queue.append(store_track)
                count += 1
            if count == 0:
                print_wrn("[Google Play Music] Operation requires " \
                          "an Unlimited subscription.")
            logging.info("Added %d Unlimited promoted tracks to queue", \
                         count)
            self.__update_play_queue_order()
        except CallFailure:
            raise RuntimeError("Operation requires an Unlimited subscription.")

    def next_url(self):
        """ Retrieve the url of the next track in the playback queue.

        """
        if len(self.queue):
            self.queue_index += 1
            if (self.queue_index < len(self.queue)) \
               and (self.queue_index >= 0):
                next_song = self.queue[self.play_queue_order[self.queue_index]]
                return self.__retrieve_track_url(next_song)
            else:
                self.queue_index = -1
                return self.next_url()
        else:
            return ''

    def prev_url(self):
        """ Retrieve the url of the previous track in the playback queue.

        """
        if len(self.queue):
            self.queue_index -= 1
            if (self.queue_index < len(self.queue)) \
               and (self.queue_index >= 0):
                prev_song = self.queue[self.play_queue_order[self.queue_index]]
                return self.__retrieve_track_url(prev_song)
            else:
                self.queue_index = len(self.queue)
                return self.prev_url()
        else:
            return ''

    def __update_play_queue_order(self):
        """ Update the queue playback order.

        A sequential order is applied if the current play mode is "NORMAL" or a
        random order if current play mode is "SHUFFLE"

        """
        total_tracks = len(self.queue)
        if total_tracks:
            if not len(self.play_queue_order):
                # Create a sequential play order, if empty
                self.play_queue_order = range(total_tracks)
            if self.current_play_mode == self.play_modes.SHUFFLE:
                random.shuffle(self.play_queue_order)
            print_nfo("[Google Play Music] [Tracks in queue] '{0}'." \
                      .format(total_tracks))

    def __retrieve_track_url(self, song):
        """ Retrieve a song url

        """
        if song.get('episodeId'):
            song_url = self.__gmusic.get_podcast_episode_stream_url(
                song['episodeId'], self.__device_id)
        else:
            song_url = self.__gmusic.get_stream_url(song['id'],
                                                    self.__device_id)

        try:
            self.now_playing_song = song
            return song_url
        except AttributeError:
            logging.info("Could not retrieve the song url!")
            raise

    def __update_local_library(self):
        """ Retrieve the songs and albums from the user's library

        """
        print_msg("[Google Play Music] [Retrieving library] : '{0}'. " \
                  .format(self.__email))

        songs = self.__gmusic.get_all_songs()
        self.playlists[self.thumbs_up_playlist_name] = list()

        # Retrieve the user's song library
        for song in songs:
            if "rating" in song and song['rating'] == "5":
                self.playlists[self.thumbs_up_playlist_name].append(song)

            song_id = song['id']
            song_artist = song['artist']
            song_album = song['album']

            self.song_map[song_id] = song

            if song_artist == "":
                song_artist = "Unknown Artist"

            if song_album == "":
                song_album = "Unknown Album"

            if song_artist not in self.library:
                self.library[song_artist] = CaseInsensitiveDict()
                self.library[song_artist][self.all_songs_album_title] = list()

            if song_album not in self.library[song_artist]:
                self.library[song_artist][song_album] = list()

            self.library[song_artist][song_album].append(song)
            self.library[song_artist][self.all_songs_album_title].append(song)

        # Sort albums by track number
        for artist in self.library.keys():
            logging.info("Artist : %s", to_ascii(artist))
            for album in self.library[artist].keys():
                logging.info("   Album : %s", to_ascii(album))
                if album == self.all_songs_album_title:
                    sorted_album = sorted(self.library[artist][album],
                                          key=lambda k: k['title'])
                else:
                    sorted_album = sorted(
                        self.library[artist][album],
                        key=lambda k: k.get('trackNumber', 0))
                self.library[artist][album] = sorted_album

    def __update_stations_unlimited(self):
        """ Retrieve stations (Unlimited)

        """
        self.stations.clear()
        stations = self.__gmusic.get_all_stations()
        self.stations[u"I'm Feeling Lucky"] = 'IFL'
        for station in stations:
            station_name = station['name']
            logging.info("station name : %s", to_ascii(station_name))
            self.stations[station_name] = station['id']

    def __enqueue_user_station_unlimited(self, arg):
        """ Enqueue a user station (Unlimited)

        """
        print_msg("[Google Play Music] [Station search "\
                  "in user's library] : '{0}'. " \
                  .format(self.__email))
        self.__update_stations_unlimited()
        station_name = arg
        station_id = None
        for name, st_id in self.stations.iteritems():
            print_nfo("[Google Play Music] [Station] '{0}'." \
                      .format(to_ascii(name)))
        if arg not in self.stations.keys():
            for name, st_id in self.stations.iteritems():
                if arg.lower() in name.lower():
                    station_id = st_id
                    station_name = name
                    break
        else:
            station_id = self.stations[arg]

        num_tracks = MAX_TRACKS
        tracks = list()
        if station_id:
            try:
                tracks = self.__gmusic.get_station_tracks(station_id, \
                                                          num_tracks)
            except KeyError:
                raise RuntimeError("Operation requires an "
                                   "Unlimited subscription.")
            tracks_added = self.__enqueue_tracks(tracks)
            if tracks_added:
                if arg.lower() != station_name.lower():
                    print_wrn("[Google Play Music] '{0}' not found. " \
                              "Playing '{1}' instead." \
                              .format(arg.encode('utf-8'), name.encode('utf-8')))
                logging.info("Added %d tracks from %s to queue", tracks_added,
                             arg)
                self.__update_play_queue_order()
            else:
                print_wrn("[Google Play Music] '{0}' has no tracks. " \
                          .format(station_name))

        if not len(self.queue):
            print_wrn("[Google Play Music] '{0}' " \
                      "not found in the user's library. " \
                      .format(arg.encode('utf-8')))

    def __enqueue_station_unlimited(self,
                                    arg,
                                    max_results=MAX_TRACKS,
                                    quiet=False):
        """Search for a station and enqueue all of its tracks (Unlimited)

        """
        if not quiet:
            print_msg("[Google Play Music] [Station search in "\
                      "Google Play Music] : '{0}'. " \
                      .format(arg.encode('utf-8')))
        try:
            station_name = arg
            station_id = None
            station = self.__gmusic_search(arg, 'station', max_results, quiet)

            if station:
                station = station['station']
                station_name = station['name']
                seed = station['seed']
                seed_type = seed['seedType']
                track_id = seed['trackId'] if seed_type == u'2' else None
                artist_id = seed['artistId'] if seed_type == u'3' else None
                album_id = seed['albumId'] if seed_type == u'4' else None
                genre_id = seed['genreId'] if seed_type == u'5' else None
                playlist_token = seed[
                    'playlistShareToken'] if seed_type == u'8' else None
                curated_station_id = seed[
                    'curatedStationId'] if seed_type == u'9' else None
                num_tracks = max_results
                tracks = list()
                try:
                    station_id \
                        = self.__gmusic.create_station(station_name, \
                                                       track_id, \
                                                       artist_id, \
                                                       album_id, \
                                                       genre_id, \
                                                       playlist_token, \
                                                       curated_station_id)
                    tracks \
                        = self.__gmusic.get_station_tracks(station_id, \
                                                           num_tracks)
                except KeyError:
                    raise RuntimeError("Operation requires an "
                                       "Unlimited subscription.")
                tracks_added = self.__enqueue_tracks(tracks)
                if tracks_added:
                    if not quiet:
                        print_wrn("[Google Play Music] [Station] : '{0}'." \
                                  .format(station_name.encode('utf-8')))
                    logging.info("Added %d tracks from %s to queue", \
                                 tracks_added, arg.encode('utf-8'))
                    self.__update_play_queue_order()

        except KeyError:
            raise KeyError("Station not found : {0}".format(arg))

    def __enqueue_situation_unlimited(self, arg):
        """Search for a situation and enqueue all of its tracks (Unlimited)

        """
        print_msg("[Google Play Music] [Situation search in "\
                  "Google Play Music] : '{0}'. " \
                  .format(arg.encode('utf-8')))
        try:
            situation_hits = self.__gmusic.search(arg)['situation_hits']

            # If the search didn't return results, just do another search with
            # an empty string
            if not len(situation_hits):
                situation_hits = self.__gmusic.search("")['situation_hits']
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(arg.encode('utf-8')))

            # Try to find a "best result", if one exists
            situation = next((hit for hit in situation_hits \
                              if 'best_result' in hit.keys() \
                              and hit['best_result'] == True), None)

            num_tracks = MAX_TRACKS

            # If there is no best result, then get a selection of tracks from
            # each situation. At least we'll play some music.
            if not situation and len(situation_hits):
                max_results = num_tracks / len(situation_hits)
                for hit in situation_hits:
                    situation = hit['situation']
                    print_nfo("[Google Play Music] [Situation] '{0} : {1}'." \
                              .format((hit['situation']['title']).encode('utf-8'),
                                      (hit['situation']['description']).encode('utf-8')))
                    self.__enqueue_station_unlimited(situation['title'],
                                                     max_results, True)
            elif situation:
                # There is at list one sitution, enqueue its tracks.
                situation = situation['situation']
                max_results = num_tracks
                self.__enqueue_station_unlimited(situation['title'],
                                                 max_results, True)

            if not situation:
                raise KeyError

        except KeyError:
            raise KeyError("Situation not found : {0}".format(arg))

    def __enqueue_podcast(self, arg):
        """Search for a podcast series and enqueue all of its tracks.

        """
        print_msg("[Google Play Music] [Podcast search in "\
                  "Google Play Music] : '{0}'. " \
                  .format(arg.encode('utf-8')))
        try:
            podcast_hits = self.__gmusic_search(arg,
                                                'podcast',
                                                10,
                                                quiet=False)

            if not podcast_hits:
                print_wrn(
                    "[Google Play Music] [Podcast] 'Search returned zero results'."
                )
                print_wrn(
                    "[Google Play Music] [Podcast] 'Are you in a supported region "
                    "(currently only US and Canada) ?'")

            # Use the first podcast retrieved. At least we'll play something.
            podcast = dict()
            if podcast_hits and len(podcast_hits):
                podcast = podcast_hits['series']

            episodes_added = 0
            if podcast:
                # There is a podcast, enqueue its episodes.
                print_nfo("[Google Play Music] [Podcast] 'Playing '{0}' by {1}'." \
                          .format((podcast['title']).encode('utf-8'),
                                  (podcast['author']).encode('utf-8')))
                print_nfo("[Google Play Music] [Podcast] '{0}'." \
                          .format((podcast['description'][0:150]).encode('utf-8')))
                series = self.__gmusic.get_podcast_series_info(
                    podcast['seriesId'])
                episodes = series['episodes']
                for episode in episodes:
                    print_nfo("[Google Play Music] [Podcast Episode] '{0} : {1}'." \
                              .format((episode['title']).encode('utf-8'),
                                      (episode['description'][0:80]).encode('utf-8')))
                episodes_added = self.__enqueue_tracks(episodes)

            if not podcast or not episodes_added:
                raise KeyError

        except KeyError:
            raise KeyError(
                "Podcast not found or no episodes found: {0}".format(arg))

    def __enqueue_tracks(self, tracks):
        """ Add tracks to the playback queue

        """
        count = 0
        for track in tracks:
            if u'id' not in track.keys() and track.get('storeId'):
                track[u'id'] = track['storeId']
            self.queue.append(track)
            count += 1
        return count

    def __update_playlists(self):
        """ Retrieve the user's playlists

        """
        plists = self.__gmusic.get_all_user_playlist_contents()
        for plist in plists:
            plist_name = plist.get('name')
            tracks = plist.get('tracks')
            if plist_name and tracks:
                logging.info("playlist name : %s", to_ascii(plist_name))
                tracks.sort(key=itemgetter('creationTimestamp'))
                self.playlists[plist_name] = list()
                for track in tracks:
                    song_id = track.get('trackId')
                    if song_id:
                        song = self.song_map.get(song_id)
                        if song:
                            self.playlists[plist_name].append(song)

    def __update_playlists_unlimited(self):
        """ Retrieve shared playlists (Unlimited)

        """
        plists_subscribed_to = [p for p in self.__gmusic.get_all_playlists() \
                                if p.get('type') == 'SHARED']
        for plist in plists_subscribed_to:
            share_tok = plist['shareToken']
            playlist_items \
                = self.__gmusic.get_shared_playlist_contents(share_tok)
            plist_name = plist['name']
            logging.info("shared playlist name : %s", to_ascii(plist_name))
            self.playlists[plist_name] = list()
            for item in playlist_items:
                try:
                    song = item['track']
                    song['id'] = item['trackId']
                    self.playlists[plist_name].append(song)
                except IndexError:
                    pass

    def __gmusic_search(self,
                        query,
                        query_type,
                        max_results=MAX_TRACKS,
                        quiet=False):
        """ Search Google Play (Unlimited)

        """

        search_results = self.__gmusic.search(query, max_results)[query_type +
                                                                  '_hits']

        # This is a workaround. Some podcast results come without these two
        # keys in the dictionary
        if query_type == "podcast" and len(search_results) \
           and not search_results[0].get('navigational_result'):
            for res in search_results:
                res[u'best_result'] = False
                res[u'navigational_result'] = False
                res[query_type] = res['series']

        result = ''
        if query_type != "playlist":
            result = next((hit for hit in search_results \
                           if 'best_result' in hit.keys() \
                           and hit['best_result'] == True), None)

        if not result and len(search_results):
            secondary_hit = None
            for hit in search_results:
                name = ''
                if hit[query_type].get('name'):
                    name = hit[query_type].get('name')
                elif hit[query_type].get('title'):
                    name = hit[query_type].get('title')
                if not quiet:
                    print_nfo("[Google Play Music] [{0}] '{1}'." \
                              .format(query_type.capitalize(),
                                      (name).encode('utf-8')))
                if query.lower() == \
                   to_ascii(name).lower():
                    result = hit
                    break
                if query.lower() in \
                   to_ascii(name).lower():
                    secondary_hit = hit
            if not result and secondary_hit:
                result = secondary_hit

        if not result and not len(search_results):
            # Do another search with an empty string
            search_results = self.__gmusic.search("")[query_type + '_hits']

        if not result and len(search_results):
            # Play some random result from the search results
            random.seed()
            result = random.choice(search_results)
            if not quiet:
                print_wrn("[Google Play Music] '{0}' not found. "\
                          "Feeling lucky?." \
                          .format(query.encode('utf-8')))

        return result