text: """
  The :class:`.CDNClient` class provides a simple API for downloading Steam content from SteamPipe
  
  Initializing :class:`.CDNClient` requires a logged in :class:`.SteamClient` instance
  
  .. code:: python
  
      mysteam = SteamClient()
      ...
      mycdn = CDNClient(mysteam)
  
  
  Getting depot manifests for an app
  
  .. code:: python
  
      >>> mycdn.get_manifests(570)
      [<CDNDepotManifest('Dota 2 Content', app_id=570, depot_id=373301, gid=6397590570861788404, creation_time='2019-06-29 16:03:11')>,
       <CDNDepotManifest('Dota 2 Content 2', app_id=570, depot_id=381451, gid=5769691971272474272, creation_time='2019-06-29 00:19:02')>,
       <CDNDepotManifest('Dota 2 Content 3', app_id=570, depot_id=381452, gid=3194393866044592918, creation_time='2019-06-27 00:05:38')>,
       <CDNDepotManifest('Dota 2 Content 4', app_id=570, depot_id=381453, gid=8005824150061180163, creation_time='2019-06-08 07:49:57')>,
       <CDNDepotManifest('Dota 2 Content 5', app_id=570, depot_id=381454, gid=9003299908441378336, creation_time='2019-06-26 18:56:19')>,
       <CDNDepotManifest('Dota 2 Content 6', app_id=570, depot_id=381455, gid=8000458746487720619, creation_time='2019-06-29 00:19:43')>,
       <CDNDepotManifest('Dota 2 Win32', app_id=570, depot_id=373302, gid=3561463682334619841, creation_time='2019-06-29 00:16:28')>,
       <CDNDepotManifest('Dota 2 Win64', app_id=570, depot_id=373303, gid=6464064782313084040, creation_time='2019-06-29 00:16:43')>,
       <CDNDepotManifest('Dota 2 Mac', app_id=570, depot_id=373304, gid=5979018571482579541, creation_time='2019-06-29 00:16:59')>,
       <CDNDepotManifest('Dota 2 English', app_id=570, depot_id=373305, gid=4435851250675935801, creation_time='2015-06-01 20:15:37')>,
       <CDNDepotManifest('Dota 2 Linux', app_id=570, depot_id=373306, gid=4859464855297921815, creation_time='2019-06-29 00:17:25')>,
       <CDNDepotManifest('Dota 2 Korean', app_id=570, depot_id=373308, gid=8598853793233320583, creation_time='2019-03-05 17:16:49')>,
       <CDNDepotManifest('Dota 2 Simplified Chinese', app_id=570, depot_id=373309, gid=6975893321745168138, creation_time='2019-06-25 21:40:37')>,
       <CDNDepotManifest('Dota 2 Russian', app_id=570, depot_id=381456, gid=5425063725991897591, creation_time='2019-03-05 17:19:53')>,
       <CDNDepotManifest('Dota 2 Workshop tools', app_id=570, depot_id=381450, gid=8629205096668418087, creation_time='2019-06-29 16:04:18')>,
       <CDNDepotManifest('Dota 2 OpenGL Windows', app_id=570, depot_id=401531, gid=6502316736107281444, creation_time='2019-06-07 19:04:08')>,
       <CDNDepotManifest('Dota 2 Vulkan Common', app_id=570, depot_id=401535, gid=6405492872419215600, creation_time='2019-06-07 19:04:11')>,
       <CDNDepotManifest('Dota 2 Vulkan Win64', app_id=570, depot_id=401536, gid=3821288251412129608, creation_time='2019-06-25 21:42:29')>,
       <CDNDepotManifest('Dota 2 Vulkan Linux64', app_id=570, depot_id=401537, gid=3144805829218032316, creation_time='2019-06-17 16:54:43')>,
       <CDNDepotManifest('Dota 2 VR', app_id=570, depot_id=313255, gid=706332602567268673, creation_time='2017-10-04 18:52:14')>,
       <CDNDepotManifest('Dota 2 Vulkan Mac', app_id=570, depot_id=401538, gid=2223235822414824351, creation_time='2019-06-11 19:37:19')>]
  
      >>> mycdn.get_manifests(570, filter_func=lambda depot_id, info: 'Dota 2 Content' in info['name'])
      [<CDNDepotManifest('Dota 2 Content', app_id=570, depot_id=373301, gid=6397590570861788404, creation_time='2019-06-29 16:03:11')>,
       <CDNDepotManifest('Dota 2 Content 2', app_id=570, depot_id=381451, gid=5769691971272474272, creation_time='2019-06-29 00:19:02')>,
       <CDNDepotManifest('Dota 2 Content 3', app_id=570, depot_id=381452, gid=3194393866044592918, creation_time='2019-06-27 00:05:38')>,
       <CDNDepotManifest('Dota 2 Content 4', app_id=570, depot_id=381453, gid=8005824150061180163, creation_time='2019-06-08 07:49:57')>,
       <CDNDepotManifest('Dota 2 Content 5', app_id=570, depot_id=381454, gid=9003299908441378336, creation_time='2019-06-26 18:56:19')>,
       <CDNDepotManifest('Dota 2 Content 6', app_id=570, depot_id=381455, gid=8000458746487720619, creation_time='2019-06-29 00:19:43')>]
  
  
  Listing files
  
  .. code:: python
  
      >>> file_list = mycdn.iter_files(570)
      >>> list(file_list)[:10]
      [<CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota_addons\\dungeon\\particles\\test_particle\\generic_attack_crit_blur_rope.vpcf_c', 2134)>,
       <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota_addons\\dungeon\\materials\\blends\\mud_brick_normal_psd_5cc4fe8b.vtex_c', 351444)>,
       <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota_addons\\hero_demo\\scripts\\vscripts\\la_spawn_enemy_at_target.lua', 1230)>,
       <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota_addons\\winter_2018\\particles\\dark_moon\\darkmoon_last_hit_effect_damage_flash_b.vpcf_c', 1386)>,
       <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota_addons\\dungeon\\scripts\\vscripts\\abilities\\siltbreaker_line_wave.lua', 3305)>,
       <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota_addons\\dungeon\\materials\\models\\heroes\\broodmother\\broodmother_body_poison.vmat_c', 10888)>,
       <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota\\resource\\cursor\\workshop\\sltv_shaker_cursor_pack\\cursor_spell_default.ani', 4362)>,
       <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota_addons\\overthrow\\panorama\\images\\custom_game\\team_icons\\team_icon_tiger_01_png.vtex_c', 18340)>,
       <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota\\resource\\cursor\\valve\\ti7\\cursor_attack_illegal.bmp', 4152)>,
       <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota_addons\\winter_2018\\models\\creeps\\ice_biome\\undeadtusk\\undead_tuskskeleton01.vmdl_c', 13516)>
  
  Reading a file directly from SteamPipe
  
  .. code:: python
  
      >>> file_list = mycdn.iter_files(570, r'game\dota\gameinfo.gi')
      >>> myfile = next(file_list)
      <CDNDepotFile(570, 373301, 6397590570861788404, 'game\\dota\\gameinfo.gi', 6808)>
      >>> print(myfile.read(80).decode('utf-8'))
      "GameInfo"
      {
              game            "Dota 2"
              title           "Dota 2"
  
              gamelogo 1
              type            multiplayer_only
      ...
  
  """
  
  from zipfile import ZipFile
  from io import BytesIO
  from collections import OrderedDict, deque
  from six import itervalues, iteritems
  from binascii import crc32, unhexlify
  from datetime import datetime
  import logging
  import struct
  
  import vdf
  from gevent.pool import Pool as GPool
  from cachetools import LRUCache
  from steam import webapi
  from steam.exceptions import SteamError
  from steam.core.msg import MsgProto
  from steam.enums import EResult, EType
  from steam.enums.emsg import EMsg
  from steam.utils.web import make_requests_session
  from steam.core.crypto import symmetric_decrypt, symmetric_decrypt_ecb
  from steam.core.manifest import DepotManifest, DepotFile
  from steam.protobufs.content_manifest_pb2 import ContentManifestPayload
  
  try:
      import lzma
  except ImportError:
      from backports import lzma
  
  def decrypt_manifest_gid_2(encrypted_gid, password):
      """Decrypt manifest gid v2 bytes
  
      :param encrypted_gid: encrypted gid v2 bytes
      :type  encrypted_gid: bytes
      :param password: encryption password
      :type  password: byt
      :return: manifest gid
      :rtype: int
      """
      return struct.unpack('<Q', symmetric_decrypt_ecb(encrypted_gid, password))[0]
  
  def get_content_servers_from_cs(cell_id, host='cs.steamcontent.com', port=80, num_servers=20, session=None):
      """Get a list of CS servers from a single CS server
  
      :param cell_id: location cell id
      :type  cell_id: bytes
      :param host: CS server host
      :type  host: str
      :param port: server port number
      :type  port: int
      :param num_servers: number of servers to return
      :type  num_servers: int
      :param session: requests Session instance
      :type  session: :class:`requests.Session`
      :return: list of CS servers
      :rtype: :class:`list` [:class:`.ContentServer`]
      """
      proto = 'https' if port == 443 else 'http'
  
      url = '%s://%s:%s/serverlist/%s/%s/' % (proto, host, port, cell_id, num_servers)
      session = make_requests_session() if session is None else session
      resp = session.get(url)
  
      if resp.status_code != 200:
          return []
  
      kv = vdf.loads(resp.text, mapper=OrderedDict)
  
      if kv.get('deferred') == '1':
          return []
  
      servers = []
  
      for entry in itervalues(kv['serverlist']):
          server = ContentServer()
          server.type = entry['type']
          server.https = True if entry['https_support'] == 'mandatory' else False
          server.host = entry['Host']
          server.vhost = entry['vhost']
          server.port = 443 if server.https else 80
          server.cell_id = entry['cell']
          server.load = entry['load']
          server.weighted_load = entry['weightedload']
          servers.append(server)
  
      return servers
  
  
  def get_content_servers_from_webapi(cell_id, num_servers=20):
      """Get a list of CS servers from Steam WebAPI
  
      :param cell_id: location cell id
      :type  cell_id: bytes
      :param num_servers: number of servers to return
      :type  num_servers: int
      :return: list of CS servers
      :rtype: :class:`list` [:class:`.ContentServer`]
      """
      params = {'cell_id': cell_id, 'max_servers': num_servers}
      resp = webapi.get('IContentServerDirectoryService', 'GetServersForSteamPipe', params=params)
  
      servers = []
  
      for entry in resp['response']['servers']:
          server = ContentServer()
          server.type = entry['type']
          server.https = True if entry['https_support'] == 'mandatory' else False
          server.host = entry['host']
          server.vhost = entry['vhost']
          server.port = 443 if server.https else 80
          server.cell_id = entry.get('cell_id', 0)
          server.load = entry['load']
          server.weighted_load = entry['weighted_load']
          servers.append(server)
  
      return servers
  
  
  class ContentServer(object):
      https = False
      host = None
      vhost = None
      port = None
      type = None
      cell_id = 0
      load = None
      weighted_load = None
  
      def __repr__(self):
          return "<%s('%s://%s:%s', type=%s, cell_id=%s)>" % (
              self.__class__.__name__,
              'https' if self.https else 'http',
              self.host,
              self.port,
              repr(self.type),
              repr(self.cell_id),
              )
  
  
  class CDNDepotFile(DepotFile):
      def __init__(self, manifest, file_mapping):
          """File-like object proxy for content files located on SteamPipe
  
          :param manifest: parrent manifest instance
          :type  manifest: :class:`.CDNDepotManifest`
          :param file_mapping: file mapping instance from manifest
          :type  file_mapping: ContentManifestPayload.FileMapping
          """
          if not isinstance(manifest, CDNDepotManifest):
              raise TypeError("Expected 'manifest' to be of type CDNDepotFile")
          if not isinstance(file_mapping, ContentManifestPayload.FileMapping):
              raise TypeError("Expected 'file_mapping' to be of type ContentManifestPayload.FileMapping")
  
          DepotFile.__init__(self, manifest, file_mapping)
  
          self.offset = 0
          self._lc = None
          self._lcbuff = b''
  
      def __repr__(self):
          return "<%s(%s, %s, %s, %s, %s)>" % (
              self.__class__.__name__,
              self.manifest.app_id,
              self.manifest.depot_id,
              self.manifest.gid,
              repr(self.filename_raw),
              'is_directory=True' if self.is_directory else self.size,
              )
  
      @property
      def seekable(self):
          """:type: bool"""
          return self.is_file
  
      def tell(self):
          """:type: int"""
          if not self.seekable:
              raise ValueError("This file is not seekable, probably because its directory or symlink")
          return self.offset
  
      def seek(self, offset, whence=0):
          """Seen file
  
          :param offset: file offset
          :type  offset: int
          :param whence: offset mode, see :meth:`io.IOBase.seek`
          :type  whence: int
          """
          if not self.seekable:
              raise ValueError("This file is not seekable, probably because its directory or symlink")
  
          if whence == 0:
              if offset < 0:
                  raise IOError("Invalid argument")
          elif whence == 1:
              offset = self.offset + offset
          elif whence == 2:
              offset = self.size + offset
          else:
              raise ValueError("Invalid value for whence")
  
          self.offset = max(0, min(self.size, offset))
  
      def _get_chunk(self, chunk):
          if not self._lc or self._lc.sha != chunk.sha:
              self._lcbuff = self.manifest.cdn_client.get_chunk(
                  self.manifest.app_id,
                  self.manifest.depot_id,
                  chunk.sha.hex(),
                  )
              self._lc = chunk
          return self._lcbuff
  
      def __iter__(self):
          return self
  
      def __next__(self):
          return self.next()
  
      def __enter__(self):
          return self
  
      def __exit__(self, type, value, traceback):
          pass
  
      def next(self):
          line = self.readline()
          if line == b'':
              raise StopIteration
          return line
  
      def read(self, length=-1):
          """Read bytes from the file
  
          :param length: number of bytes to read. Read the whole file if not set
          :type  length: int
          :returns: file data
          :rtype: bytes
          """
          if length == -1:
              length = self.size - self.offset
          if length == 0 or self.offset >= self.size or self.size == 0:
              return b''
  
          end_offset = self.offset + length
  
          # we cache last chunk to allow small length reads and local seek
          if (self._lc
             and self.offset >= self._lc.offset
             and end_offset <= self._lc.offset + self._lc.cb_original):
              data = self._lcbuff[self.offset - self._lc.offset:self.offset - self._lc.offset + length]
          # if we need to read outside the bounds of the cached chunk
          # we go to loop over chunks to determine which to download
          else:
              data = BytesIO()
              start_offset = None
  
              # Manifest orders the chunks by offset in ascending order
              for chunk in self.chunks:
                  if chunk.offset >= end_offset:
                      break
  
                  chunk_start = chunk.offset
                  chunk_end = chunk_start + chunk.cb_original
  
                  if (     chunk_start <= self.offset <  chunk_end
                        or chunk_start <  end_offset  <= chunk_end):
                      if start_offset is None:
                          start_offset = chunk.offset
                      data.write(self._get_chunk(chunk))
  
              data.seek(self.offset - start_offset)
              data = data.read(length)
  
          self.offset = min(self.size, end_offset)
          return data
  
      def readline(self):
          """Read a single line
  
          :return: single file line
          :rtype: bytes
          """
          buf = b''
  
          for chunk in iter(lambda: self.read(256), b''):
              pos = chunk.find(b'\n')
              if pos > -1:
                  pos += 1  # include \n
                  buf += chunk[:pos]
                  self.seek(self.offset - (len(chunk) - pos))
                  break
  
              buf += chunk
  
          return buf
  
      def readlines(self):
          """Get file contents as list of lines
  
          :return: list of lines
          :rtype: :class:`list` [:class:`bytes`]
          """
          return [line for line in self]
  
  
  class CDNDepotManifest(DepotManifest):
      DepotFileClass = CDNDepotFile
      name = None  #: set only by :meth:`CDNClient.get_manifests`
  
      def __init__(self, cdn_client, app_id, data):
          """Holds manifest metadata and file list.
  
          :param cdn_client: CDNClient instance
          :type  cdn_client: :class:`.CDNClient`
          :param app_id: App ID
          :type  app_id: int
          :param data: serialized manifest data
          :type  data: bytes
          """
          self.cdn_client = cdn_client
          self.app_id = app_id
          DepotManifest.__init__(self, data)
  
      def __repr__(self):
          params = ', '.join([
                      "app_id=" + str(self.app_id),
                      "depot_id=" + str(self.depot_id),
                      "gid=" + str(self.gid),
                      "creation_time=" + repr(
                          datetime.utcfromtimestamp(self.metadata.creation_time).isoformat().replace('T', ' ')
                          ),
                      ])
  
          if self.name:
              params = repr(self.name) + ', ' + params
  
          if self.filenames_encrypted:
              params += ', filenames_encrypted=True'
  
          return "<%s(%s)>" % (
              self.__class__.__name__,
              params,
              )
  
      def deserialize(self, data):
          DepotManifest.deserialize(self, data)
  
          # order chunks in ascending order by their offset
          # required for CDNDepotFile
          for mapping in self.payload.mappings:
              mapping.chunks.sort(key=lambda x: x.offset, reverse=False)
  
  
  class CDNClient(object):
      DepotManifestClass = CDNDepotManifest
      _LOG = logging.getLogger("CDNClient")
      servers = deque()  #: CS Server list
      _chunk_cache = LRUCache(20)
      cell_id = 0  #: Cell ID to use, initialized from SteamClient instance
  
      def __init__(self, client):
          """CDNClient allows loading and reading of manifests for Steam apps are used
          to list and download content
  
          :param client: logged in SteamClient instance
          :type  client: :class:`.SteamClient`
          """
          self.gpool = GPool(8)            #: task pool
          self.steam = client              #: SteamClient instance
          if self.steam:
              self.cell_id = self.steam.cell_id
  
          self.web = make_requests_session()
          self.depot_keys = {}             #: depot decryption keys
          self.manifests = {}              #: CDNDepotManifest instances
          self.app_depots = {}             #: app depot info
          self.beta_passwords = {}         #: beta branch decryption keys
          self.licensed_app_ids = set()    #: app_ids that the SteamClient instance has access to
          self.licensed_depot_ids = set()  #: depot_ids that the SteamClient instance has access to
  
          if not self.servers:
              self.fetch_content_servers()
  
          self.load_licenses()
  
      def clear_cache(self):
          """Cleared cached information. Next call on methods with caching will return fresh data"""
          self.manifests.clear()
          self.app_depots.clear()
          self.beta_passwords.clear()
  
      def load_licenses(self):
          """Read licenses from SteamClient instance, required for determining accessible content"""
          self.licensed_app_ids.clear()
          self.licensed_depot_ids.clear()
  
          if self.steam.steam_id.type == EType.AnonUser:
              packages = [17906]
          else:
              if not self.steam.licenses:
                  self._LOG.debug("No steam licenses found on SteamClient instance")
                  return
  
              packages = list(map(lambda l: {'packageid': l.package_id, 'access_token': l.access_token},
                                  itervalues(self.steam.licenses)))
  
          for package_id, info in iteritems(self.steam.get_product_info(packages=packages)['packages']):
              self.licensed_app_ids.update(info['appids'].values())
              self.licensed_depot_ids.update(info['depotids'].values())
  
      def fetch_content_servers(self, num_servers=20):
          """Update CS server list
  
          :param num_servers: numbers of CS server to fetch
          :type  num_servers: int
          """
          self.servers.clear()
  
          self._LOG.debug("Trying to fetch content servers from Steam API")
  
          servers = get_content_servers_from_webapi(self.cell_id)
          servers = filter(lambda server: server.type != 'OpenCache', servers) # see #264
          self.servers.extend(servers)
  
          if not self.servers:
              raise SteamError("Failed to fetch content servers")
  
      def get_content_server(self, rotate=False):
          """Get a CS server for content download
  
          :param rotate: forcefully rotate server list and get a new server
          :type  rotate: bool
          """
          if rotate:
              self.servers.rotate(-1)
          return self.servers[0]
  
      def get_depot_key(self, app_id, depot_id):
          """Get depot key, which is needed to decrypt files
  
          :param app_id: app id
          :type  app_id: int
          :param depot_id: depot id
          :type  depot_id: int
          :return: returns decryption key
          :rtype: bytes
          :raises SteamError: error message
          """
          if depot_id not in self.depot_keys:
              msg = self.steam.get_depot_key(app_id, depot_id)
  
              if msg and msg.eresult == EResult.OK:
                  self.depot_keys[depot_id] = msg.depot_encryption_key
              else:
                  raise SteamError("Failed getting depot key",
                                   EResult.Timeout if msg is None else EResult(msg.eresult))
  
          return self.depot_keys[depot_id]
  
      def cdn_cmd(self, command, args):
          """Run CDN command request
  
          :param command: command name
          :type  command: str
          :param args: args
          :type  args: str
          :returns: requests response
          :rtype: :class:`requests.Response`
          :raises SteamError: on error
          """
          server = self.get_content_server()
  
          while True:
              url = "%s://%s:%s/%s/%s" % (
                  'https' if server.https else 'http',
                  server.host,
                  server.port,
                  command,
                  args,
                  )
  
              try:
                  resp = self.web.get(url, timeout=10)
              except Exception as exp:
                  self._LOG.debug("Request error: %s", exp)
              else:
                  if resp.ok:
                      return resp
                  elif 400 <= resp.status_code < 500:
                      self._LOG.debug("Got HTTP %s", resp.status_code)
                      raise SteamError("HTTP Error %s" % resp.status_code)
                  self.steam.sleep(0.5)
  
              server = self.get_content_server(rotate=True)
  
      def get_chunk(self, app_id, depot_id, chunk_id):
          """Download a single content chunk
  
          :param app_id: App ID
          :type  app_id: int
          :param depot_id: Depot ID
          :type  depot_id: int
          :param chunk_id: Chunk ID
          :type  chunk_id: int
          :returns: chunk data
          :rtype: bytes
          :raises SteamError: error message
          """
          if (depot_id, chunk_id) not in self._chunk_cache:
              resp = self.cdn_cmd('depot', '%s/chunk/%s' % (depot_id, chunk_id))
  
              data = symmetric_decrypt(resp.content, self.get_depot_key(app_id, depot_id))
  
              if data[:2] == b'VZ':
                  if data[-2:] != b'zv':
                      raise SteamError("VZ: Invalid footer: %s" % repr(data[-2:]))
                  if data[2:3] != b'a':
                      raise SteamError("VZ: Invalid version: %s" % repr(data[2:3]))
  
                  vzfilter = lzma._decode_filter_properties(lzma.FILTER_LZMA1, data[7:12])
                  vzdec = lzma.LZMADecompressor(lzma.FORMAT_RAW, filters=[vzfilter])
                  checksum, decompressed_size = struct.unpack('<II', data[-10:-2])
                  # decompress_size is needed since lzma will sometime produce longer output
                  # [12:-9] is need as sometimes lzma will produce shorter output
                  # together they get us the right data
                  data = vzdec.decompress(data[12:-9])[:decompressed_size]
                  if crc32(data) != checksum:
                      raise SteamError("VZ: CRC32 checksum doesn't match for decompressed data")
              else:
                  with ZipFile(BytesIO(data)) as zf:
                      data = zf.read(zf.filelist[0])
  
              self._chunk_cache[(depot_id, chunk_id)] = data
  
          return self._chunk_cache[(depot_id, chunk_id)]
  
      def get_manifest(self, app_id, depot_id, manifest_gid, decrypt=True):
          """Download a manifest file
  
          :param app_id: App ID
          :type  app_id: int
          :param depot_id: Depot ID
          :type  depot_id: int
          :param manifest_gid: Manifest gid
          :type  manifest_gid: int
          :param decrypt: Decrypt manifest filenames
          :type  decrypt: bool
          :returns: manifest instance
          :rtype: :class:`.CDNDepotManifest`
          """
          if (app_id, depot_id, manifest_gid) not in self.manifests:
              resp = self.cdn_cmd('depot', '%s/manifest/%s/5' % (depot_id, manifest_gid))
  
              if resp.ok:
                  manifest = self.DepotManifestClass(self, app_id, resp.content)
                  if decrypt:
                      manifest.decrypt_filenames(self.get_depot_key(app_id, depot_id))
                  self.manifests[(app_id, depot_id, manifest_gid)] = manifest
  
          return self.manifests[(app_id, depot_id, manifest_gid)]
  
      def check_beta_password(self, app_id, password):
          """Check branch beta password to unlock encrypted branches
  
          :param app_id: App ID
          :type  app_id: int
          :param password: beta password
          :type  password: str
          :returns: result
          :rtype: :class:`.EResult`
          """
          resp = self.steam.send_job_and_wait(MsgProto(EMsg.ClientCheckAppBetaPassword),
                                              {'app_id': app_id, 'betapassword': password})
  
          if resp.eresult == EResult.OK:
              self._LOG.debug("Unlocked following beta branches: %s",
                              ', '.join(map(lambda x: x.betaname.lower(), resp.betapasswords)))
              for entry in resp.betapasswords:
                  self.beta_passwords[(app_id, entry.betaname.lower())] = unhexlify(entry.betapassword)
          else:
              self._LOG.debug("App beta password check failed. %r" % EResult(resp.eresult))
  
          return EResult(resp.eresult)
  
      def get_app_depot_info(self, app_id):
          if app_id not in self.app_depots:
              self.app_depots[app_id] = self.steam.get_product_info([app_id])['apps'][app_id]['depots']
          return self.app_depots[app_id]
  
      def get_manifests(self, app_id, branch='public', password=None, filter_func=None, decrypt=True):
          """Get a list of CDNDepotManifest for app
  
          :param app_id: App ID
          :type  app_id: int
          :param branch: branch name
          :type  branch: str
          :param password: branch password for locked branches
          :type  password: str
          :param filter_func:
              Function to filter depots. ``func(depot_id, depot_info)``
          :returns: list of :class:`.CDNDepotManifest`
          :rtype: :class:`list` [:class:`.CDNDepotManifest`]
          :raises SteamError: error message
          """
          depots = self.get_app_depot_info(app_id)
  
          is_enc_branch = False
  
          if branch not in depots.get('branches', {}):
              raise SteamError("No branch named %s for app_id %s" % (repr(branch), app_id))
          elif int(depots['branches'][branch].get('pwdrequired', 0)) > 0:
              is_enc_branch = True
  
              if (app_id, branch) not in self.beta_passwords:
                  if not password:
                      raise SteamError("Branch %r requires a password" % branch)
  
                  result = self.check_beta_password(app_id, password)
  
                  if result != EResult.OK:
                      raise SteamError("Branch password is not valid. %r" % result)
  
                  if (app_id, branch) not in self.beta_passwords:
                      raise SteamError("Incorrect password for branch %r" % branch)
  
          def async_fetch_manifest(app_id, depot_id, manifest_gid, decrypt, name):
              manifest = self.get_manifest(app_id, depot_id, manifest_gid, decrypt)
              manifest.name = name
              return manifest
  
          tasks = []
          shared_depots = {}
  
          for depot_id, depot_info in iteritems(depots):
              if not depot_id.isdigit():
                  continue
  
              depot_id = int(depot_id)
  
              # if filter_func set, use it to filter the list the depots
              if filter_func and not filter_func(depot_id, depot_info):
                  continue
  
              # if we have no license for the depot, no point trying as we won't get depot_key
              if (decrypt
                 and depot_id not in self.licensed_depot_ids
                 and depot_id not in self.licensed_app_ids):
                  self._LOG.debug("No license for depot %s (%s). Skipping...",
                                  repr(depot_info['name']),
                                  depot_id,
                                  )
                  continue
  
              # accumulate the shared depots
              if 'depotfromapp' in depot_info:
                  shared_depots.setdefault(int(depot_info['depotfromapp']), set()).add(depot_id)
                  continue
  
  
              # process depot, and get manifest for branch
              if is_enc_branch:
                  egid = depot_info.get('encryptedmanifests', {}).get(branch, {}).get('encrypted_gid_2')
  
                  if egid is not None:
                      manifest_gid = decrypt_manifest_gid_2(unhexlify(egid),
                                                            self.beta_passwords[(app_id, branch)])
                  else:
                      manifest_gid = depot_info.get('manifests', {}).get('public')
              else:
                  manifest_gid = depot_info.get('manifests', {}).get(branch)
  
              if manifest_gid is not None:
                  tasks.append(self.gpool.spawn(async_fetch_manifest,
                                                app_id,
                                                depot_id,
                                                manifest_gid,
                                                decrypt,
                                                depot_info['name'],
                                                ))
  
          # collect results
          manifests = []
  
          for task in tasks:
              manifests.append(task.get())
  #           try:
  #               result = task.get()
  #           except SteamError as exp:
  #               self._LOG.error("Error: %s", exp)
  #               raise
  #           else:
  #               if isinstance(result, list):
  #                   manifests.extend(result)
  #               else:
  #                   manifests.append(result)
  
          # load shared depot manifests
          for app_id, depot_ids in iteritems(shared_depots):
              def nested_ffunc(depot_id, depot_info, depot_ids=depot_ids, ffunc=filter_func):
                  return (int(depot_id) in depot_ids
                          and (ffunc is None or ffunc(depot_id,  depot_info)))
  
              manifests += self.get_manifests(app_id, filter_func=nested_ffunc)
  
          return manifests
  
      def iter_files(self, app_id, filename_filter=None, branch='public', password=None, filter_func=None):
          """Like :meth:`.get_manifests` but returns a iterator that goes through all the files
          in all the manifest.
  
          :param app_id: App ID
          :type  app_id: int
          :param filename_filter: wildcard filter for file paths
          :type  branch: str
          :param branch: branch name
          :type  branch: str
          :param password: branch password for locked branches
          :type  password: str
          :param filter_func:
              Function to filter depots. ``func(depot_id, depot_info)``
          :returns: generator of of CDN files
          :rtype: [:class:`.CDNDepotFile`]
          """
          for manifest in self.get_manifests(app_id, branch, password, filter_func):
              for fp in manifest.iter_files(filename_filter):
                  yield fp
  
      def get_manifest_for_workshop_item(self, item_id):
          """Get the manifest file for a worshop item that is hosted on SteamPipe
  
          :param item_id: Workshop ID
          :type  item_id: int
          :returns: manifest instance
          :rtype: :class:`.CDNDepotManifest`
          :raises SteamError: error message
          """
          resp = self.steam.send_um_and_wait('PublishedFile.GetDetails#1', {
              'publishedfileids': [item_id],
              'includetags': False,
              'includeadditionalpreviews': False,
              'includechildren': False,
              'includekvtags': False,
              'includevotes': False,
              'short_description': True,
              'includeforsaledata': False,
              'includemetadata': False,
              'language': 0
          }, timeout=7)
  
          if resp.header.eresult != EResult.OK:
              raise SteamError(resp.header.error_message or 'No message', resp.header.eresult)
  
          wf = None if resp is None else resp.body.publishedfiledetails[0]
  
          if wf is None or wf.result != EResult.OK:
              raise SteamError("Failed getting workshop file info",
                                EResult.Timeout if resp is None else EResult(wf.result))
          elif not wf.hcontent_file:
              raise SteamError("Workshop file is not on SteamPipe", EResult.FileNotFound)
  
          app_id = ws_app_id = wf.consumer_appid
  
          manifest = self.get_manifest(app_id, ws_app_id, wf.hcontent_file)
          manifest.name = wf.title
          return manifest
  
  
  