# coding=utf-8
# --------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
#
# Code generated by Microsoft (R) AutoRest Code Generator.
# Changes may cause incorrect behavior and will be lost if the code is
# regenerated.
# --------------------------------------------------------------------------

from msrest.pipeline import ClientRawResponse

from .. import models

class CustomInstanceOperations(object):
    """CustomInstanceOperations operations.

    :param client: Client for service requests.
    :param config: Configuration of service client.
    :param serializer: An object model serializer.
    :param deserializer: An object model deserializer.
    :ivar x_bing_apis_sdk: Activate swagger compliance. Constant value: "true".
    """

    models = models

    def __init__(self, client, config, serializer, deserializer):

        self._client = client
        self._serialize = serializer
        self._deserialize = deserializer

        self.config = config
        self.x_bing_apis_sdk = "true"

    def image_search(
            self, custom_config, query, accept_language=None, user_agent=None, client_id=None, client_ip=None, location=None, aspect=None, color=None, country_code=None, count=None, freshness=None, height=None, id=None, image_content=None, image_type=None, license=None, market=None, max_file_size=None, max_height=None, max_width=None, min_file_size=None, min_height=None, min_width=None, offset=None, safe_search=None, size=None, set_lang=None, width=None, custom_headers=None, raw=False, **operation_config):
        """The Custom Image Search API lets you send an image search query to Bing
        and get image results found in your custom view of the web.

        :param custom_config: The identifier for the custom search
         configuration
        :type custom_config: long
        :param query: The user's search query term. The term cannot be empty.
         The term may contain [Bing Advanced
         Operators](http://msdn.microsoft.com/library/ff795620.aspx). For
         example, to limit images to a specific domain, use the
         [site:](http://msdn.microsoft.com/library/ff795613.aspx) operator. To
         help improve relevance of an insights query (see
         [insightsToken](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#insightstoken)),
         you should always include the user's query term. Use this parameter
         only with the Image Search API.Do not specify this parameter when
         calling the Trending Images API.
        :type query: str
        :param accept_language: A comma-delimited list of one or more
         languages to use for user interface strings. The list is in decreasing
         order of preference. For additional information, including expected
         format, see
         [RFC2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html).
         This header and the
         [setLang](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#setlang)
         query parameter are mutually exclusive; do not specify both. If you
         set this header, you must also specify the
         [cc](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#cc)
         query parameter. To determine the market to return results for, Bing
         uses the first supported language it finds from the list and combines
         it with the cc parameter value. If the list does not include a
         supported language, Bing finds the closest language and market that
         supports the request or it uses an aggregated or default market for
         the results. To determine the market that Bing used, see the
         BingAPIs-Market header. Use this header and the cc query parameter
         only if you specify multiple languages. Otherwise, use the
         [mkt](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#mkt)
         and
         [setLang](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#setlang)
         query parameters. A user interface string is a string that's used as a
         label in a user interface. There are few user interface strings in the
         JSON response objects. Any links to Bing.com properties in the
         response objects apply the specified language.
        :type accept_language: str
        :param user_agent: The user agent originating the request. Bing uses
         the user agent to provide mobile users with an optimized experience.
         Although optional, you are encouraged to always specify this header.
         The user-agent should be the same string that any commonly used
         browser sends. For information about user agents, see [RFC
         2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html). The
         following are examples of user-agent strings. Windows Phone:
         Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0;
         IEMobile/10.0; ARM; Touch; NOKIA; Lumia 822). Android: Mozilla / 5.0
         (Linux; U; Android 2.3.5; en - us; SCH - I500 Build / GINGERBREAD)
         AppleWebKit / 533.1 (KHTML; like Gecko) Version / 4.0 Mobile Safari /
         533.1. iPhone: Mozilla / 5.0 (iPhone; CPU iPhone OS 6_1 like Mac OS X)
         AppleWebKit / 536.26 (KHTML; like Gecko) Mobile / 10B142 iPhone4; 1
         BingWeb / 3.03.1428.20120423. PC: Mozilla / 5.0 (Windows NT 6.3;
         WOW64; Trident / 7.0; Touch; rv:11.0) like Gecko. iPad: Mozilla / 5.0
         (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit / 537.51.1 (KHTML, like
         Gecko) Version / 7.0 Mobile / 11A465 Safari / 9537.53
        :type user_agent: str
        :param client_id: Bing uses this header to provide users with
         consistent behavior across Bing API calls. Bing often flights new
         features and improvements, and it uses the client ID as a key for
         assigning traffic on different flights. If you do not use the same
         client ID for a user across multiple requests, then Bing may assign
         the user to multiple conflicting flights. Being assigned to multiple
         conflicting flights can lead to an inconsistent user experience. For
         example, if the second request has a different flight assignment than
         the first, the experience may be unexpected. Also, Bing can use the
         client ID to tailor web results to that client ID’s search history,
         providing a richer experience for the user. Bing also uses this header
         to help improve result rankings by analyzing the activity generated by
         a client ID. The relevance improvements help with better quality of
         results delivered by Bing APIs and in turn enables higher
         click-through rates for the API consumer. IMPORTANT: Although
         optional, you should consider this header required. Persisting the
         client ID across multiple requests for the same end user and device
         combination enables 1) the API consumer to receive a consistent user
         experience, and 2) higher click-through rates via better quality of
         results from the Bing APIs. Each user that uses your application on
         the device must have a unique, Bing generated client ID. If you do not
         include this header in the request, Bing generates an ID and returns
         it in the X-MSEdge-ClientID response header. The only time that you
         should NOT include this header in a request is the first time the user
         uses your app on that device. Use the client ID for each Bing API
         request that your app makes for this user on the device. Persist the
         client ID. To persist the ID in a browser app, use a persistent HTTP
         cookie to ensure the ID is used across all sessions. Do not use a
         session cookie. For other apps such as mobile apps, use the device's
         persistent storage to persist the ID. The next time the user uses your
         app on that device, get the client ID that you persisted. Bing
         responses may or may not include this header. If the response includes
         this header, capture the client ID and use it for all subsequent Bing
         requests for the user on that device. If you include the
         X-MSEdge-ClientID, you must not include cookies in the request.
        :type client_id: str
        :param client_ip: The IPv4 or IPv6 address of the client device. The
         IP address is used to discover the user's location. Bing uses the
         location information to determine safe search behavior. Although
         optional, you are encouraged to always specify this header and the
         X-Search-Location header. Do not obfuscate the address (for example,
         by changing the last octet to 0). Obfuscating the address results in
         the location not being anywhere near the device's actual location,
         which may result in Bing serving erroneous results.
        :type client_ip: str
        :param location: A semicolon-delimited list of key/value pairs that
         describe the client's geographical location. Bing uses the location
         information to determine safe search behavior and to return relevant
         local content. Specify the key/value pair as <key>:<value>. The
         following are the keys that you use to specify the user's location.
         lat (required): The latitude of the client's location, in degrees. The
         latitude must be greater than or equal to -90.0 and less than or equal
         to +90.0. Negative values indicate southern latitudes and positive
         values indicate northern latitudes. long (required): The longitude of
         the client's location, in degrees. The longitude must be greater than
         or equal to -180.0 and less than or equal to +180.0. Negative values
         indicate western longitudes and positive values indicate eastern
         longitudes. re (required): The radius, in meters, which specifies the
         horizontal accuracy of the coordinates. Pass the value returned by the
         device's location service. Typical values might be 22m for GPS/Wi-Fi,
         380m for cell tower triangulation, and 18,000m for reverse IP lookup.
         ts (optional): The UTC UNIX timestamp of when the client was at the
         location. (The UNIX timestamp is the number of seconds since January
         1, 1970.) head (optional): The client's relative heading or direction
         of travel. Specify the direction of travel as degrees from 0 through
         360, counting clockwise relative to true north. Specify this key only
         if the sp key is nonzero. sp (optional): The horizontal velocity
         (speed), in meters per second, that the client device is traveling.
         alt (optional): The altitude of the client device, in meters. are
         (optional): The radius, in meters, that specifies the vertical
         accuracy of the coordinates. Specify this key only if you specify the
         alt key. Although many of the keys are optional, the more information
         that you provide, the more accurate the location results are. Although
         optional, you are encouraged to always specify the user's geographical
         location. Providing the location is especially important if the
         client's IP address does not accurately reflect the user's physical
         location (for example, if the client uses VPN). For optimal results,
         you should include this header and the X-MSEdge-ClientIP header, but
         at a minimum, you should include this header.
        :type location: str
        :param aspect: Filter images by the following aspect ratios. All: Do
         not filter by aspect.Specifying this value is the same as not
         specifying the aspect parameter. Square: Return images with standard
         aspect ratio. Wide: Return images with wide screen aspect ratio. Tall:
         Return images with tall aspect ratio. Possible values include: 'All',
         'Square', 'Wide', 'Tall'
        :type aspect: str or
         ~azure.cognitiveservices.search.customimagesearch.models.ImageAspect
        :param color: Filter images by the following color options. ColorOnly:
         Return color images. Monochrome: Return black and white images. Return
         images with one of the following dominant colors: Black, Blue, Brown,
         Gray, Green, Orange, Pink, Purple, Red, Teal, White, Yellow. Possible
         values include: 'ColorOnly', 'Monochrome', 'Black', 'Blue', 'Brown',
         'Gray', 'Green', 'Orange', 'Pink', 'Purple', 'Red', 'Teal', 'White',
         'Yellow'
        :type color: str or
         ~azure.cognitiveservices.search.customimagesearch.models.ImageColor
        :param country_code: A 2-character country code of the country where
         the results come from. For a list of possible values, see [Market
         Codes](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#market-codes).
         If you set this parameter, you must also specify the
         [Accept-Language](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#acceptlanguage)
         header. Bing uses the first supported language it finds from the
         languages list, and combine that language with the country code that
         you specify to determine the market to return results for. If the
         languages list does not include a supported language, Bing finds the
         closest language and market that supports the request, or it may use
         an aggregated or default market for the results instead of a specified
         one. You should use this query parameter and the Accept-Language query
         parameter only if you specify multiple languages; otherwise, you
         should use the mkt and setLang query parameters. This parameter and
         the
         [mkt](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#mkt)
         query parameter are mutually exclusive—do not specify both.
        :type country_code: str
        :param count: The number of images to return in the response. The
         actual number delivered may be less than requested. The default is 35.
         The maximum value is 150. You use this parameter along with the offset
         parameter to page results.For example, if your user interface displays
         20 images per page, set count to 20 and offset to 0 to get the first
         page of results.For each subsequent page, increment offset by 20 (for
         example, 0, 20, 40). Use this parameter only with the Image Search
         API.Do not specify this parameter when calling the Insights, Trending
         Images, or Web Search APIs.
        :type count: int
        :param freshness: Filter images by the following discovery options.
         Day: Return images discovered by Bing within the last 24 hours. Week:
         Return images discovered by Bing within the last 7 days. Month: Return
         images discovered by Bing within the last 30 days. Possible values
         include: 'Day', 'Week', 'Month'
        :type freshness: str or
         ~azure.cognitiveservices.search.customimagesearch.models.Freshness
        :param height: Filter images that have the specified height, in
         pixels. You may use this filter with the size filter to return small
         images that have a height of 150 pixels.
        :type height: int
        :param id: An ID that uniquely identifies an image. Use this parameter
         to ensure that the specified image is the first image in the list of
         images that Bing returns. The
         [Image](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#image)
         object's imageId field contains the ID that you set this parameter to.
        :type id: str
        :param image_content: Filter images by the following content types.
         Face: Return images that show only a person's face. Portrait: Return
         images that show only a person's head and shoulders. Possible values
         include: 'Face', 'Portrait'
        :type image_content: str or
         ~azure.cognitiveservices.search.customimagesearch.models.ImageContent
        :param image_type: Filter images by the following image types.
         AnimatedGif: Return only animated GIFs. Clipart: Return only clip art
         images. Line: Return only line drawings. Photo: Return only
         photographs(excluding line drawings, animated Gifs, and clip art).
         Shopping: Return only images that contain items where Bing knows of a
         merchant that is selling the items. This option is valid in the en -
         US market only.Transparent: Return only images with a transparent
         background. Possible values include: 'AnimatedGif', 'Clipart', 'Line',
         'Photo', 'Shopping', 'Transparent'
        :type image_type: str or
         ~azure.cognitiveservices.search.customimagesearch.models.ImageType
        :param license: Filter images by the following license types. All: Do
         not filter by license type.Specifying this value is the same as not
         specifying the license parameter. Any: Return images that are under
         any license type. The response doesn't include images that do not
         specify a license or the license is unknown. Public: Return images
         where the creator has waived their exclusive rights, to the fullest
         extent allowed by law. Share: Return images that may be shared with
         others. Changing or editing the image might not be allowed. Also,
         modifying, sharing, and using the image for commercial purposes might
         not be allowed. Typically, this option returns the most images.
         ShareCommercially: Return images that may be shared with others for
         personal or commercial purposes. Changing or editing the image might
         not be allowed. Modify: Return images that may be modified, shared,
         and used. Changing or editing the image might not be allowed.
         Modifying, sharing, and using the image for commercial purposes might
         not be allowed. ModifyCommercially: Return images that may be
         modified, shared, and used for personal or commercial purposes.
         Typically, this option returns the fewest images. For more information
         about these license types, see [Filter Images By License
         Type](http://go.microsoft.com/fwlink/?LinkId=309768). Possible values
         include: 'All', 'Any', 'Public', 'Share', 'ShareCommercially',
         'Modify', 'ModifyCommercially'
        :type license: str or
         ~azure.cognitiveservices.search.customimagesearch.models.ImageLicense
        :param market: The market where the results come from. Typically, mkt
         is the country where the user is making the request from. However, it
         could be a different country if the user is not located in a country
         where Bing delivers results. The market must be in the form <language
         code>-<country code>. For example, en-US. The string is case
         insensitive. For a list of possible market values, see [Market
         Codes](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#market-codes).
         NOTE: If known, you are encouraged to always specify the market.
         Specifying the market helps Bing route the request and return an
         appropriate and optimal response. If you specify a market that is not
         listed in [Market
         Codes](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#market-codes),
         Bing uses a best fit market code based on an internal mapping that is
         subject to change. This parameter and the
         [cc](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#cc)
         query parameter are mutually exclusive—do not specify both.
        :type market: str
        :param max_file_size: Filter images that are less than or equal to the
         specified file size. The maximum file size that you may specify is
         520,192 bytes. If you specify a larger value, the API uses 520,192. It
         is possible that the response may include images that are slightly
         larger than the specified maximum. You may specify this filter and
         minFileSize to filter images within a range of file sizes.
        :type max_file_size: long
        :param max_height: Filter images that have a height that is less than
         or equal to the specified height. Specify the height in pixels. You
         may specify this filter and minHeight to filter images within a range
         of heights. This filter and the height filter are mutually exclusive.
        :type max_height: long
        :param max_width: Filter images that have a width that is less than or
         equal to the specified width. Specify the width in pixels. You may
         specify this filter and maxWidth to filter images within a range of
         widths. This filter and the width filter are mutually exclusive.
        :type max_width: long
        :param min_file_size: Filter images that are greater than or equal to
         the specified file size. The maximum file size that you may specify is
         520,192 bytes. If you specify a larger value, the API uses 520,192. It
         is possible that the response may include images that are slightly
         smaller than the specified minimum. You may specify this filter and
         maxFileSize to filter images within a range of file sizes.
        :type min_file_size: long
        :param min_height: Filter images that have a height that is greater
         than or equal to the specified height. Specify the height in pixels.
         You may specify this filter and maxHeight to filter images within a
         range of heights. This filter and the height filter are mutually
         exclusive.
        :type min_height: long
        :param min_width: Filter images that have a width that is greater than
         or equal to the specified width. Specify the width in pixels. You may
         specify this filter and maxWidth to filter images within a range of
         widths. This filter and the width filter are mutually exclusive.
        :type min_width: long
        :param offset: The zero-based offset that indicates the number of
         images to skip before returning images. The default is 0. The offset
         should be less than
         ([totalEstimatedMatches](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#totalestimatedmatches)
         - count). Use this parameter along with the count parameter to page
         results. For example, if your user interface displays 20 images per
         page, set count to 20 and offset to 0 to get the first page of
         results. For each subsequent page, increment offset by 20 (for
         example, 0, 20, 40). It is possible for multiple pages to include some
         overlap in results. To prevent duplicates, see
         [nextOffset](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#nextoffset).
         Use this parameter only with the Image API. Do not specify this
         parameter when calling the Trending Images API or the Web Search API.
        :type offset: long
        :param safe_search: Filter images for adult content. The following are
         the possible filter values. Off: May return images with adult content.
         If the request is through the Image Search API, the response includes
         thumbnail images that are clear (non-fuzzy). However, if the request
         is through the Web Search API, the response includes thumbnail images
         that are pixelated (fuzzy). Moderate: If the request is through the
         Image Search API, the response doesn't include images with adult
         content. If the request is through the Web Search API, the response
         may include images with adult content (the thumbnail images are
         pixelated (fuzzy)). Strict: Do not return images with adult content.
         The default is Moderate. If the request comes from a market that
         Bing's adult policy requires that safeSearch is set to Strict, Bing
         ignores the safeSearch value and uses Strict. If you use the site:
         query operator, there is the chance that the response may contain
         adult content regardless of what the safeSearch query parameter is set
         to. Use site: only if you are aware of the content on the site and
         your scenario supports the possibility of adult content. Possible
         values include: 'Off', 'Moderate', 'Strict'
        :type safe_search: str or
         ~azure.cognitiveservices.search.customimagesearch.models.SafeSearch
        :param size: Filter images by the following sizes. All: Do not filter
         by size. Specifying this value is the same as not specifying the size
         parameter. Small: Return images that are less than 200x200 pixels.
         Medium: Return images that are greater than or equal to 200x200 pixels
         but less than 500x500 pixels. Large: Return images that are 500x500
         pixels or larger. Wallpaper: Return wallpaper images. You may use this
         parameter along with the height or width parameters. For example, you
         may use height and size to request small images that are 150 pixels
         tall. Possible values include: 'All', 'Small', 'Medium', 'Large',
         'Wallpaper'
        :type size: str or
         ~azure.cognitiveservices.search.customimagesearch.models.ImageSize
        :param set_lang: The language to use for user interface strings.
         Specify the language using the ISO 639-1 2-letter language code. For
         example, the language code for English is EN. The default is EN
         (English). Although optional, you should always specify the language.
         Typically, you set setLang to the same language specified by mkt
         unless the user wants the user interface strings displayed in a
         different language. This parameter and the
         [Accept-Language](https://docs.microsoft.com/en-us/rest/api/cognitiveservices/bing-images-api-v7-reference#acceptlanguage)
         header are mutually exclusive; do not specify both. A user interface
         string is a string that's used as a label in a user interface. There
         are few user interface strings in the JSON response objects. Also, any
         links to Bing.com properties in the response objects apply the
         specified language.
        :type set_lang: str
        :param width: Filter images that have the specified width, in pixels.
         You may use this filter with the size filter to return small images
         that have a width of 150 pixels.
        :type width: int
        :param dict custom_headers: headers that will be added to the request
        :param bool raw: returns the direct response alongside the
         deserialized response
        :param operation_config: :ref:`Operation configuration
         overrides<msrest:optionsforoperations>`.
        :return: Images or ClientRawResponse if raw=true
        :rtype:
         ~azure.cognitiveservices.search.customimagesearch.models.Images or
         ~msrest.pipeline.ClientRawResponse
        :raises:
         :class:`ErrorResponseException<azure.cognitiveservices.search.customimagesearch.models.ErrorResponseException>`
        """
        # Construct URL
        url = self.image_search.metadata['url']

        # Construct parameters
        query_parameters = {}
        query_parameters['customConfig'] = self._serialize.query("custom_config", custom_config, 'long', minimum=0)
        if aspect is not None:
            query_parameters['aspect'] = self._serialize.query("aspect", aspect, 'str')
        if color is not None:
            query_parameters['color'] = self._serialize.query("color", color, 'str')
        if country_code is not None:
            query_parameters['cc'] = self._serialize.query("country_code", country_code, 'str')
        if count is not None:
            query_parameters['count'] = self._serialize.query("count", count, 'int')
        if freshness is not None:
            query_parameters['freshness'] = self._serialize.query("freshness", freshness, 'str')
        if height is not None:
            query_parameters['height'] = self._serialize.query("height", height, 'int')
        if id is not None:
            query_parameters['id'] = self._serialize.query("id", id, 'str')
        if image_content is not None:
            query_parameters['imageContent'] = self._serialize.query("image_content", image_content, 'str')
        if image_type is not None:
            query_parameters['imageType'] = self._serialize.query("image_type", image_type, 'str')
        if license is not None:
            query_parameters['license'] = self._serialize.query("license", license, 'str')
        if market is not None:
            query_parameters['mkt'] = self._serialize.query("market", market, 'str')
        if max_file_size is not None:
            query_parameters['maxFileSize'] = self._serialize.query("max_file_size", max_file_size, 'long')
        if max_height is not None:
            query_parameters['maxHeight'] = self._serialize.query("max_height", max_height, 'long')
        if max_width is not None:
            query_parameters['maxWidth'] = self._serialize.query("max_width", max_width, 'long')
        if min_file_size is not None:
            query_parameters['minFileSize'] = self._serialize.query("min_file_size", min_file_size, 'long')
        if min_height is not None:
            query_parameters['minHeight'] = self._serialize.query("min_height", min_height, 'long')
        if min_width is not None:
            query_parameters['minWidth'] = self._serialize.query("min_width", min_width, 'long')
        if offset is not None:
            query_parameters['offset'] = self._serialize.query("offset", offset, 'long')
        query_parameters['q'] = self._serialize.query("query", query, 'str')
        if safe_search is not None:
            query_parameters['safeSearch'] = self._serialize.query("safe_search", safe_search, 'str')
        if size is not None:
            query_parameters['size'] = self._serialize.query("size", size, 'str')
        if set_lang is not None:
            query_parameters['setLang'] = self._serialize.query("set_lang", set_lang, 'str')
        if width is not None:
            query_parameters['width'] = self._serialize.query("width", width, 'int')

        # Construct headers
        header_parameters = {}
        header_parameters['Content-Type'] = 'application/json; charset=utf-8'
        if custom_headers:
            header_parameters.update(custom_headers)
        header_parameters['X-BingApis-SDK'] = self._serialize.header("self.x_bing_apis_sdk", self.x_bing_apis_sdk, 'str')
        if accept_language is not None:
            header_parameters['Accept-Language'] = self._serialize.header("accept_language", accept_language, 'str')
        if user_agent is not None:
            header_parameters['User-Agent'] = self._serialize.header("user_agent", user_agent, 'str')
        if client_id is not None:
            header_parameters['X-MSEdge-ClientID'] = self._serialize.header("client_id", client_id, 'str')
        if client_ip is not None:
            header_parameters['X-MSEdge-ClientIP'] = self._serialize.header("client_ip", client_ip, 'str')
        if location is not None:
            header_parameters['X-Search-Location'] = self._serialize.header("location", location, 'str')

        # Construct and send request
        request = self._client.get(url, query_parameters)
        response = self._client.send(request, header_parameters, stream=False, **operation_config)

        if response.status_code not in [200]:
            raise models.ErrorResponseException(self._deserialize, response)

        deserialized = None

        if response.status_code == 200:
            deserialized = self._deserialize('Images', response)

        if raw:
            client_raw_response = ClientRawResponse(deserialized, response)
            return client_raw_response

        return deserialized
    image_search.metadata = {'url': '/images/search'}


import ast
import logging
import operator
import re
import time
import requests.cookies

from functools import partial
from collections import OrderedDict

from streamlink.cache import Cache
from streamlink.exceptions import PluginError, NoStreamsError, FatalPluginError
from streamlink.options import Options, Arguments

log = logging.getLogger(__name__)

# FIXME: This is a crude attempt at making a bitrate's
# weight end up similar to the weight of a resolution.
# Someone who knows math, please fix.
BIT_RATE_WEIGHT_RATIO = 2.8

ALT_WEIGHT_MOD = 0.01

QUALITY_WEIGTHS_EXTRA = {
    "other": {
        "live": 1080,
    },
    "tv": {
        "hd": 1080,
        "sd": 576,
    },
    "quality": {
        "ehq": 720,
        "hq": 576,
        "sq": 360,
    },
}

FILTER_OPERATORS = {
    "<": operator.lt,
    "<=": operator.le,
    ">": operator.gt,
    ">=": operator.ge,
}

PARAMS_REGEX = r"(\w+)=({.+?}|\[.+?\]|\(.+?\)|'(?:[^'\\]|\\')*'|\"(?:[^\"\\]|\\\")*\"|\S+)"

HIGH_PRIORITY = 30
NORMAL_PRIORITY = 20
LOW_PRIORITY = 10
NO_PRIORITY = 0

def stream_weight(stream):
    for group, weights in QUALITY_WEIGTHS_EXTRA.items():
        if stream in weights:
            return weights[stream], group

    match = re.match(r"^(\d+)(k|p)?(\d+)?(\+)?(?:[a_](\d+)k)?(?:_(alt)(\d)?)?$", stream)

    if match:
        weight = 0

        if match.group(6):
            if match.group(7):
                weight -= ALT_WEIGHT_MOD * int(match.group(7))
            else:
                weight -= ALT_WEIGHT_MOD

        name_type = match.group(2)
        if name_type == "k":  # bit rate
            bitrate = int(match.group(1))
            weight += bitrate / BIT_RATE_WEIGHT_RATIO

            return weight, "bitrate"

        elif name_type == "p":  # resolution
            weight += int(match.group(1))

            if match.group(3):  # fps eg. 60p or 50p
                weight += int(match.group(3))

            if match.group(4) == "+":
                weight += 1

            if match.group(5):  # bit rate classifier for resolution
                weight += int(match.group(5)) / BIT_RATE_WEIGHT_RATIO

            return weight, "pixels"

    return 0, "none"

def iterate_streams(streams):
    for name, stream in streams:
        if isinstance(stream, list):
            for sub_stream in stream:
                yield (name, sub_stream)
        else:
            yield (name, stream)

def stream_type_priority(stream_types, stream):
    stream_type = type(stream[1]).shortname()

    try:
        prio = stream_types.index(stream_type)
    except ValueError:
        try:
            prio = stream_types.index("*")
        except ValueError:
            prio = 99

    return prio

def stream_sorting_filter(expr, stream_weight):
    match = re.match(r"(?P<op><=|>=|<|>)?(?P<value>[\w+]+)", expr)

    if not match:
        raise PluginError("Invalid filter expression: {0}".format(expr))

    op, value = match.group("op", "value")
    op = FILTER_OPERATORS.get(op, operator.eq)
    filter_weight, filter_group = stream_weight(value)

    def func(quality):
        weight, group = stream_weight(quality)

        if group == filter_group:
            return not op(weight, filter_weight)

        return True

    return func

def parse_url_params(url):
    split = url.split(" ", 1)
    url = split[0]
    params = split[1] if len(split) > 1 else ''
    return url, parse_params(params)

def parse_params(params):
    rval = {}
    matches = re.findall(PARAMS_REGEX, params)

    for key, value in matches:
        try:
            value = ast.literal_eval(value)
        except Exception:
            pass

        rval[key] = value

    return rval

class UserInputRequester(object):
    """
    Base Class / Interface for requesting user input

    eg. From the console
    """
    def ask(self, prompt):
        """
        Ask the user for a text input, the input is not sensitive
        and can be echoed to the user

        :param prompt: message to display when asking for the input
        :return: the value the user input
        """
        raise NotImplementedError

    def ask_password(self, prompt):
        """
        Ask the user for a text input, the input _is_ sensitive
        and should be masked as the user gives the input

        :param prompt: message to display when asking for the input
        :return: the value the user input
        """
        raise NotImplementedError

class Plugin(object):
    """A plugin can retrieve stream information from the URL specified.

    :param url: URL that the plugin will operate on
    """

    cache = None
    logger = None
    module = "unknown"
    options = Options()
    arguments = Arguments()
    session = None
    _user_input_requester = None

    @classmethod
    def bind(cls, session, module, user_input_requester=None):
        cls.cache = Cache(filename="plugin-cache.json",
                          key_prefix=module)
        cls.logger = logging.getLogger("streamlink.plugin." + module)
        cls.module = module
        cls.session = session
        if user_input_requester is not None:
            if isinstance(user_input_requester, UserInputRequester):
                cls._user_input_requester = user_input_requester
            else:
                raise RuntimeError("user-input-requester must be an instance of UserInputRequester")

    def __init__(self, url):
        self.url = url
        try:
            self.load_cookies()
        except RuntimeError:
            pass  # unbound cannot load

    @classmethod
    def can_handle_url(cls, url):
        raise NotImplementedError

    @classmethod
    def set_option(cls, key, value):
        cls.options.set(key, value)

    @classmethod
    def get_option(cls, key):
        return cls.options.get(key)

    @classmethod
    def get_argument(cls, key):
        return cls.arguments.get(key)

    @classmethod
    def stream_weight(cls, stream):
        return stream_weight(stream)

    @classmethod
    def default_stream_types(cls, streams):
        stream_types = ["rtmp", "hls", "hds", "http"]

        for name, stream in iterate_streams(streams):
            stream_type = type(stream).shortname()

            if stream_type not in stream_types:
                stream_types.append(stream_type)

        return stream_types

    @classmethod
    def broken(cls, issue=None):
        def func(*args, **kwargs):
            msg = (
                "This plugin has been marked as broken. This is likely due to "
                "changes to the service preventing a working implementation. "
            )

            if issue:
                msg += "More info: https://github.com/streamlink/streamlink/issues/{0}".format(issue)

            raise PluginError(msg)

        def decorator(*args, **kwargs):
            return func

        return decorator

    @classmethod
    def priority(cls, url):
        """
        Return the plugin priority for a given URL, by default it returns
        NORMAL priority.
        :return: priority level
        """
        return NORMAL_PRIORITY

    def streams(self, stream_types=None, sorting_excludes=None):
        """Attempts to extract available streams.

        Returns a :class:`dict` containing the streams, where the key is
        the name of the stream, most commonly the quality and the value
        is a :class:`Stream` object.

        The result can contain the synonyms **best** and **worst** which
        points to the streams which are likely to be of highest and
        lowest quality respectively.

        If multiple streams with the same name are found, the order of
        streams specified in *stream_types* will determine which stream
        gets to keep the name while the rest will be renamed to
        "<name>_<stream type>".

        The synonyms can be fine tuned with the *sorting_excludes*
        parameter. This can be either of these types:

            - A list of filter expressions in the format
              *[operator]<value>*. For example the filter ">480p" will
              exclude streams ranked higher than "480p" from the list
              used in the synonyms ranking. Valid operators are >, >=, <
              and <=. If no operator is specified then equality will be
              tested.

            - A function that is passed to filter() with a list of
              stream names as input.

        :param stream_types: A list of stream types to return.
        :param sorting_excludes: Specify which streams to exclude from
                                 the best/worst synonyms.

        """

        try:
            ostreams = self._get_streams()
            if isinstance(ostreams, dict):
                ostreams = ostreams.items()

            # Flatten the iterator to a list so we can reuse it.
            if ostreams:
                ostreams = list(ostreams)
        except NoStreamsError:
            return {}
        except (IOError, OSError, ValueError) as err:
            raise PluginError(err)

        if not ostreams:
            return {}

        if stream_types is None:
            stream_types = self.default_stream_types(ostreams)

        # Add streams depending on stream type and priorities
        sorted_streams = sorted(iterate_streams(ostreams),
                                key=partial(stream_type_priority,
                                            stream_types))

        streams = {}
        for name, stream in sorted_streams:
            stream_type = type(stream).shortname()

            # Use * as wildcard to match other stream types
            if "*" not in stream_types and stream_type not in stream_types:
                continue

            # drop _alt from any stream names
            if name.endswith("_alt"):
                name = name[:-len("_alt")]

            existing = streams.get(name)
            if existing:
                existing_stream_type = type(existing).shortname()
                if existing_stream_type != stream_type:
                    name = "{0}_{1}".format(name, stream_type)

                if name in streams:
                    name = "{0}_alt".format(name)
                    num_alts = len(list(filter(lambda n: n.startswith(name), streams.keys())))

                    # We shouldn't need more than 2 alt streams
                    if num_alts >= 2:
                        continue
                    elif num_alts > 0:
                        name = "{0}{1}".format(name, num_alts + 1)

            # Validate stream name and discard the stream if it's bad.
            match = re.match("([A-z0-9_+]+)", name)
            if match:
                name = match.group(1)
            else:
                self.logger.debug("The stream '{0}' has been ignored "
                                  "since it is badly named.", name)
                continue

            # Force lowercase name and replace space with underscore.
            streams[name.lower()] = stream

        # Create the best/worst synonmys
        def stream_weight_only(s):
            return (self.stream_weight(s)[0] or
                    (len(streams) == 1 and 1))

        stream_names = filter(stream_weight_only, streams.keys())
        sorted_streams = sorted(stream_names, key=stream_weight_only)
        unfiltered_sorted_streams = sorted_streams

        if isinstance(sorting_excludes, list):
            for expr in sorting_excludes:
                filter_func = stream_sorting_filter(expr, self.stream_weight)
                sorted_streams = list(filter(filter_func, sorted_streams))
        elif callable(sorting_excludes):
            sorted_streams = list(filter(sorting_excludes, sorted_streams))

        final_sorted_streams = OrderedDict()

        for stream_name in sorted(streams, key=stream_weight_only):
            final_sorted_streams[stream_name] = streams[stream_name]

        if len(sorted_streams) > 0:
            best = sorted_streams[-1]
            worst = sorted_streams[0]
            final_sorted_streams["worst"] = streams[worst]
            final_sorted_streams["best"] = streams[best]
        elif len(unfiltered_sorted_streams) > 0:
            best = unfiltered_sorted_streams[-1]
            worst = unfiltered_sorted_streams[0]
            final_sorted_streams["worst-unfiltered"] = streams[worst]
            final_sorted_streams["best-unfiltered"] = streams[best]

        return final_sorted_streams

    def _get_streams(self):
        raise NotImplementedError

    def get_title(self):
        return None

    def get_author(self):
        return None

    def get_category(self):
        return None

    def save_cookies(self, cookie_filter=None, default_expires=60 * 60 * 24 * 7):
        """
        Store the cookies from ``http`` in the plugin cache until they expire. The cookies can be filtered
        by supplying a filter method. eg. ``lambda c: "auth" in c.name``. If no expiry date is given in the
        cookie then the ``default_expires`` value will be used.

        :param cookie_filter: a function to filter the cookies
        :type cookie_filter: function
        :param default_expires: time (in seconds) until cookies with no expiry will expire
        :type default_expires: int
        :return: list of the saved cookie names
        """
        if not self.session or not self.cache:
            raise RuntimeError("Cannot cache cookies in unbound plugin")

        cookie_filter = cookie_filter or (lambda c: True)
        saved = []

        for cookie in filter(cookie_filter, self.session.http.cookies):
            cookie_dict = {}
            for attr in ("version", "name", "value", "port", "domain", "path", "secure", "expires", "discard",
                         "comment", "comment_url", "rfc2109"):
                cookie_dict[attr] = getattr(cookie, attr, None)
            cookie_dict["rest"] = getattr(cookie, "rest", getattr(cookie, "_rest", None))

            expires = default_expires
            if cookie_dict['expires']:
                expires = int(cookie_dict['expires'] - time.time())
            key = "__cookie:{0}:{1}:{2}:{3}".format(cookie.name,
                                                    cookie.domain,
                                                    cookie.port_specified and cookie.port or "80",
                                                    cookie.path_specified and cookie.path or "*")
            self.cache.set(key, cookie_dict, expires)
            saved.append(cookie.name)

        if saved:
            self.logger.debug("Saved cookies: {0}".format(", ".join(saved)))
        return saved

    def load_cookies(self):
        """
        Load any stored cookies for the plugin that have not expired.

        :return: list of the restored cookie names
        """
        if not self.session or not self.cache:
            raise RuntimeError("Cannot loaded cached cookies in unbound plugin")

        restored = []

        for key, value in self.cache.get_all().items():
            if key.startswith("__cookie"):
                cookie = requests.cookies.create_cookie(**value)
                self.session.http.cookies.set_cookie(cookie)
                restored.append(cookie.name)

        if restored:
            self.logger.debug("Restored cookies: {0}".format(", ".join(restored)))
        return restored

    def clear_cookies(self, cookie_filter=None):
        """
        Removes all of the saved cookies for this Plugin. To filter the cookies that are deleted
        specify the ``cookie_filter`` argument (see :func:`save_cookies`).

        :param cookie_filter: a function to filter the cookies
        :type cookie_filter: function
        :return: list of the removed cookie names
        """
        if not self.session or not self.cache:
            raise RuntimeError("Cannot loaded cached cookies in unbound plugin")

        cookie_filter = cookie_filter or (lambda c: True)
        removed = []

        for key, value in sorted(self.cache.get_all().items(), key=operator.itemgetter(0), reverse=True):
            if key.startswith("__cookie"):
                cookie = requests.cookies.create_cookie(**value)
                if cookie_filter(cookie):
                    del self.session.http.cookies[cookie.name]
                    self.cache.set(key, None, 0)
                    removed.append(key)

        return removed

    def input_ask(self, prompt):
        if self._user_input_requester:
            try:
                return self._user_input_requester.ask(prompt)
            except IOError as e:
                raise FatalPluginError("User input error: {0}".format(e))
            except NotImplementedError:  # ignore this and raise a FatalPluginError
                pass
        raise FatalPluginError("This plugin requires user input, however it is not supported on this platform")

    def input_ask_password(self, prompt):
        if self._user_input_requester:
            try:
                return self._user_input_requester.ask_password(prompt)
            except IOError as e:
                raise FatalPluginError("User input error: {0}".format(e))
            except NotImplementedError:  # ignore this and raise a FatalPluginError
                pass
        raise FatalPluginError("This plugin requires user input, however it is not supported on this platform")

__all__ = ["Plugin"]


import os
import re
import sys

is_py2 = (sys.version_info[0] == 2)
is_py3 = (sys.version_info[0] == 3)
is_win32 = os.name == "nt"

if is_py2:
    input = raw_input
    stdout = sys.stdout
    file = file
    _find_unsafe = re.compile(r"[^\w@%+=:,./-]").search

    from backports.shutil_get_terminal_size import get_terminal_size

elif is_py3:
    input = input
    stdout = sys.stdout.buffer
    from io import IOBase as file
    from shutil import get_terminal_size
    from shlex import quote

    _find_unsafe = re.compile(r"[^\w@%+=:,./-]", re.ASCII).search

def shlex_quote(s):
    """Return a shell-escaped version of the string *s*.

    Backported from Python 3.3 standard library module shlex.
    """

    if is_py3:  # use the latest version instead of backporting if it's available
        return quote(s)

    if not s:
        return "''"
    if _find_unsafe(s) is None:
        return s

    # use single quotes, and put single quotes into double quotes
    # the string $'b is then quoted as '$'"'"'b'
    return "'" + s.replace("'", "'\"'\"'") + "'"

__all__ = ["is_py2", "is_py3", "is_win32", "input", "stdout", "file",
           "shlex_quote", "get_terminal_size"]


import sys

from collections import deque
from time import time

from ..compat import is_win32, get_terminal_size

PROGRESS_FORMATS = (
    "[download][{prefix}] Written {written} ({elapsed} @ {speed}/s)",
    "[download] Written {written} ({elapsed} @ {speed}/s)",
    "[download] {written} ({elapsed} @ {speed}/s)",
    "[download] {written} ({elapsed})",
    "[download] {written}"
)

# widths generated from
# http://www.unicode.org/Public/4.0-Update/EastAsianWidth-4.0.0.txt

widths = [
    (13, 1),    (15, 0),    (126, 1),   (159, 0),   (687, 1),   (710, 0),
    (711, 1),   (727, 0),   (733, 1),   (879, 0),   (1154, 1),  (1161, 0),
    (4347, 1),  (4447, 2),  (7467, 1),  (7521, 0),  (8369, 1),  (8426, 0),
    (9000, 1),  (9002, 2),  (11021, 1), (12350, 2), (12351, 1), (12438, 2),
    (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1), (64106, 2),
    (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1),
    (65510, 2), (120831, 1), (262141, 2), (1114109, 1)
]

def get_width(o):
    """Returns the screen column width for unicode ordinal."""
    for num, wid in widths:
        if o <= num:
            return wid
    return 1

def terminal_width(value):
    """Returns the width of the string it would be when displayed."""
    if isinstance(value, bytes):
        value = value.decode("utf8", "ignore")
    return sum(map(get_width, map(ord, value)))

def get_cut_prefix(value, max_len):
    """Drops Characters by unicode not by bytes."""
    should_convert = isinstance(value, bytes)
    if should_convert:
        value = value.decode("utf8", "ignore")
    for i in range(len(value)):
        if terminal_width(value[i:]) <= max_len:
            break
    return value[i:].encode("utf8", "ignore") if should_convert else value[i:]

def print_inplace(msg):
    """Clears out the previous line and prints a new one."""
    term_width = get_terminal_size().columns
    spacing = term_width - terminal_width(msg)

    # On windows we need one less space or we overflow the line for some reason.
    if is_win32:
        spacing -= 1

    sys.stderr.write("\r{0}".format(msg))
    sys.stderr.write(" " * max(0, spacing))
    sys.stderr.flush()

def format_filesize(size):
    """Formats the file size into a human readable format."""
    for suffix in ("bytes", "KB", "MB", "GB", "TB"):
        if size < 1024.0:
            if suffix in ("GB", "TB"):
                return "{0:3.2f} {1}".format(size, suffix)
            else:
                return "{0:3.1f} {1}".format(size, suffix)

        size /= 1024.0

def format_time(elapsed):
    """Formats elapsed seconds into a human readable format."""
    hours = int(elapsed / (60 * 60))
    minutes = int((elapsed % (60 * 60)) / 60)
    seconds = int(elapsed % 60)

    rval = ""
    if hours:
        rval += "{0}h".format(hours)

    if elapsed > 60:
        rval += "{0}m".format(minutes)

    rval += "{0}s".format(seconds)
    return rval

def create_status_line(**params):
    """Creates a status line with appropriate size."""
    max_size = get_terminal_size().columns - 1

    for fmt in PROGRESS_FORMATS:
        status = fmt.format(**params)

        if len(status) <= max_size:
            break

    return status

def progress(iterator, prefix):
    """Progress an iterator and updates a pretty status line to the terminal.

    The status line contains:
     - Amount of data read from the iterator
     - Time elapsed
     - Average speed, based on the last few seconds.
    """
    if terminal_width(prefix) > 25:
        prefix = (".." + get_cut_prefix(prefix, 23))
    speed_updated = start = time()
    speed_written = written = 0
    speed_history = deque(maxlen=5)

    for data in iterator:
        yield data

        now = time()
        elapsed = now - start
        written += len(data)

        speed_elapsed = now - speed_updated
        if speed_elapsed >= 0.5:
            speed_history.appendleft((
                written - speed_written,
                speed_updated,
            ))
            speed_updated = now
            speed_written = written

            speed_history_written = sum(h[0] for h in speed_history)
            speed_history_elapsed = now - speed_history[-1][1]
            speed = speed_history_written / speed_history_elapsed

            status = create_status_line(
                prefix=prefix,
                written=format_filesize(written),
                elapsed=format_time(elapsed),
                speed=format_filesize(speed)
            )
            print_inplace(status)
    sys.stderr.write("\n")
    sys.stderr.flush()


from __future__ import unicode_literals

import copy
import logging
import datetime
import re
import time

from collections import defaultdict, namedtuple
from itertools import repeat, count

import math
from isodate import parse_datetime, parse_duration, Duration
from contextlib import contextmanager
from streamlink.compat import urlparse, urljoin, urlunparse, izip, urlsplit, urlunsplit

if hasattr(datetime, "timezone"):
    utc = datetime.timezone.utc
else:
    class UTC(datetime.tzinfo):
        def utcoffset(self, dt):
            return datetime.timedelta(0)

        def tzname(self, dt):
            return "UTC"

        def dst(self, dt):
            return datetime.timedelta(0)

    utc = UTC()

log = logging.getLogger(__name__)
epoch_start = datetime.datetime(1970, 1, 1, tzinfo=utc)

class Segment(object):
    def __init__(self, url, duration, init=False, content=True, available_at=epoch_start, range=None):
        self.url = url
        self.duration = duration
        self.init = init
        self.content = content
        self.available_at = available_at
        self.range = range

def datetime_to_seconds(dt):
    return (dt - epoch_start).total_seconds()

def count_dt(firstval=datetime.datetime.now(tz=utc), step=datetime.timedelta(seconds=1)):
    x = firstval
    while True:
        yield x
        x += step

@contextmanager
def freeze_timeline(mpd):
    timelines = copy.copy(mpd.timelines)
    yield
    mpd.timelines = timelines

@contextmanager
def sleeper(duration):
    s = time.time()
    yield
    time_to_sleep = duration - (time.time() - s)
    if time_to_sleep > 0:
        time.sleep(time_to_sleep)

def sleep_until(walltime):
    c = datetime.datetime.now(tz=utc)
    time_to_wait = (walltime - c).total_seconds()
    if time_to_wait > 0:
        time.sleep(time_to_wait)

class MPDParsers(object):
    @staticmethod
    def bool_str(v):
        return v.lower() == "true"

    @staticmethod
    def type(type_):
        if type_ not in (u"static", u"dynamic"):
            raise MPDParsingError("@type must be static or dynamic")
        return type_

    @staticmethod
    def duration(duration):
        return parse_duration(duration)

    @staticmethod
    def datetime(dt):
        return parse_datetime(dt).replace(tzinfo=utc)

    @staticmethod
    def segment_template(url_template):
        end = 0
        res = ""
        for m in re.compile(r"(.*?)\$(\w+)(?:%([\w.]+))?\$").finditer(url_template):
            _, end = m.span()
            res += "{0}{{{1}{2}}}".format(m.group(1),
                                          m.group(2),
                                          (":" + m.group(3)) if m.group(3) else "")
        return (res + url_template[end:]).format

    @staticmethod
    def frame_rate(frame_rate):
        if "/" in frame_rate:
            a, b = frame_rate.split("/")
            return float(a) / float(b)
        else:
            return float(frame_rate)

    @staticmethod
    def timedelta(timescale=1):
        def _timedelta(seconds):
            return datetime.timedelta(seconds=int(float(seconds) / float(timescale)))

        return _timedelta

    @staticmethod
    def range(range_spec):
        r = range_spec.split("-")
        if len(r) != 2:
            raise MPDParsingError("invalid byte-range-spec")

        start, end = int(r[0]), r[1] and int(r[1]) or None
        return start, end and ((end-start) + 1)

class MPDParsingError(Exception):
    pass

class MPDNode(object):
    __tag__ = None

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        self.node = node
        self.root = root
        self.parent = parent
        self._base_url = kwargs.get(u"base_url")
        self.attributes = set([])
        if self.__tag__ and self.node.tag.lower() != self.__tag__.lower():
            raise MPDParsingError("root tag did not match the expected tag: {}".format(self.__tag__))

    @property
    def attrib(self):
        return self.node.attrib

    @property
    def text(self):
        return self.node.text

    def __str__(self):
        return "<{tag} {attrs}>".format(
            tag=self.__tag__,
            attrs=" ".join("@{}={}".format(attr, getattr(self, attr)) for attr in self.attributes)
        )

    def attr(self, key, default=None, parser=None, required=False, inherited=False):
        self.attributes.add(key)
        if key in self.attrib:
            value = self.attrib.get(key)
            if parser and callable(parser):
                return parser(value)
            else:
                return value
        elif inherited:
            if self.parent and hasattr(self.parent, key) and getattr(self.parent, key):
                return getattr(self.parent, key)

        if required:
            raise MPDParsingError("could not find required attribute {tag}@{attr} ".format(attr=key, tag=self.__tag__))
        else:
            return default

    def children(self, cls, minimum=0, maximum=None):

        children = self.node.findall(cls.__tag__)
        if len(children) < minimum or (maximum and len(children) > maximum):
            raise MPDParsingError("expected to find {}/{} required [{}..{})".format(
                self.__tag__, cls.__tag__, minimum, maximum or "unbound"))

        return list(map(lambda x: cls(x[1], root=self.root, parent=self, i=x[0], base_url=self.base_url),
                        enumerate(children)))

    def only_child(self, cls, minimum=0):
        children = self.children(cls, minimum=minimum, maximum=1)
        return children[0] if len(children) else None

    def walk_back(self, cls=None, f=lambda x: x):
        node = self.parent
        while node:
            if cls is None or cls.__tag__ == node.__tag__:
                yield f(node)
            node = node.parent

    def walk_back_get_attr(self, attr):
        parent_attrs = [getattr(n, attr) for n in self.walk_back() if hasattr(n, attr)]
        return parent_attrs[0] if len(parent_attrs) else None

    @property
    def base_url(self):
        base_url = self._base_url
        if hasattr(self, "baseURLs") and len(self.baseURLs):
            base_url = BaseURL.join(base_url, self.baseURLs[0].url)
        return base_url

class MPD(MPDNode):
    """
    Represents the MPD as a whole

    Should validate the XML input and provide methods to get segment URLs for each Period, AdaptationSet and
    Representation.

    """
    __tag__ = u"MPD"

    def __init__(self, node, root=None, parent=None, url=None, *args, **kwargs):
        # top level has no parent
        super(MPD, self).__init__(node, root=self, *args, **kwargs)
        # parser attributes
        self.url = url
        self.timelines = defaultdict(lambda: -1)
        self.timelines.update(kwargs.pop("timelines", {}))
        self.id = self.attr(u"id")
        self.profiles = self.attr(u"profiles", required=True)
        self.type = self.attr(u"type", default=u"static", parser=MPDParsers.type)
        self.minimumUpdatePeriod = self.attr(u"minimumUpdatePeriod", parser=MPDParsers.duration, default=Duration())
        self.minBufferTime = self.attr(u"minBufferTime", parser=MPDParsers.duration, required=True)
        self.timeShiftBufferDepth = self.attr(u"timeShiftBufferDepth", parser=MPDParsers.duration)
        self.availabilityStartTime = self.attr(u"availabilityStartTime", parser=MPDParsers.datetime,
                                               default=datetime.datetime.fromtimestamp(0, utc),  # earliest date
                                               required=self.type == "dynamic")
        self.publishTime = self.attr(u"publishTime", parser=MPDParsers.datetime, required=self.type == "dynamic")
        self.availabilityEndTime = self.attr(u"availabilityEndTime", parser=MPDParsers.datetime)
        self.mediaPresentationDuration = self.attr(u"mediaPresentationDuration", parser=MPDParsers.duration)
        self.suggestedPresentationDelay = self.attr(u"suggestedPresentationDelay", parser=MPDParsers.duration)

        # parse children
        location = self.children(Location)
        self.location = location[0] if location else None
        if self.location:
            self.url = self.location.text
            urlp = list(urlparse(self.url))
            if urlp[2]:
                urlp[2], _ = urlp[2].rsplit("/", 1)
            self._base_url = urlunparse(urlp)

        self.baseURLs = self.children(BaseURL)
        self.periods = self.children(Period, minimum=1)
        self.programInformation = self.children(ProgramInformation)

class ProgramInformation(MPDNode):
    __tag__ = "ProgramInformation"

class BaseURL(MPDNode):
    __tag__ = "BaseURL"

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        super(BaseURL, self).__init__(node, root, parent, *args, **kwargs)
        self.url = self.node.text.strip()

    @property
    def is_absolute(self):
        return urlparse(self.url).scheme

    @staticmethod
    def join(url, other):
        # if the other URL is an absolute url, then return that
        if urlparse(other).scheme:
            return other
        elif url:
            parts = list(urlsplit(url))
            if not parts[2].endswith("/"):
                parts[2] += "/"
            url = urlunsplit(parts)
            return urljoin(url, other)
        else:
            return other

class Location(MPDNode):
    __tag__ = "Location"

class Period(MPDNode):
    __tag__ = u"Period"

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        super(Period, self).__init__(node, root, parent, *args, **kwargs)
        self.i = kwargs.get(u"i", 0)
        self.id = self.attr(u"id")
        self.bitstreamSwitching = self.attr(u"bitstreamSwitching", parser=MPDParsers.bool_str)
        self.duration = self.attr(u"duration", default=Duration(), parser=MPDParsers.duration)
        self.start = self.attr(u"start", default=Duration(), parser=MPDParsers.duration)

        if self.start is None and self.i == 0 and self.root.type == "static":
            self.start = 0

        # TODO: Early Access Periods

        self.baseURLs = self.children(BaseURL)
        self.segmentBase = self.only_child(SegmentBase)
        self.adaptationSets = self.children(AdaptationSet, minimum=1)
        self.segmentList = self.only_child(SegmentList)
        self.segmentTemplate = self.only_child(SegmentTemplate)
        self.sssetIdentifier = self.only_child(AssetIdentifier)
        self.eventStream = self.children(EventStream)
        self.subset = self.children(Subset)

class SegmentBase(MPDNode):
    __tag__ = "SegmentBase"

class AssetIdentifier(MPDNode):
    __tag__ = "AssetIdentifier"

class Subset(MPDNode):
    __tag__ = "Subset"

class EventStream(MPDNode):
    __tag__ = "EventStream"

class Initialization(MPDNode):
    __tag__ = "Initialization"

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        super(Initialization, self).__init__(node, root, parent, *args, **kwargs)
        self.source_url = self.attr("sourceURL")

class SegmentURL(MPDNode):
    __tag__ = "SegmentURL"

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        super(SegmentURL, self).__init__(node, root, parent, *args, **kwargs)
        self.media = self.attr("media")
        self.media_range = self.attr("mediaRange", parser=MPDParsers.range)

class SegmentList(MPDNode):
    __tag__ = "SegmentList"

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        super(SegmentList, self).__init__(node, root, parent, *args, **kwargs)

        self.presentation_time_offset = self.attr("presentationTimeOffset")
        self.timescale = self.attr("timescale", parser=int)
        self.duration = self.attr("duration", parser=int)
        self.start_number = self.attr("startNumber", parser=int, default=1)

        if self.duration:
            self.duration_seconds = self.duration / float(self.timescale)
        else:
            self.duration_seconds = None

        self.initialization = self.only_child(Initialization)
        self.segment_urls = self.children(SegmentURL, minimum=1)

    @property
    def segments(self):
        if self.initialization:
            yield Segment(self.make_url(self.initialization.source_url), 0, init=True, content=False)
        for n, segment_url in enumerate(self.segment_urls, self.start_number):
            yield Segment(self.make_url(segment_url.media), self.duration_seconds, range=segment_url.media_range)

    def make_url(self, url):
        return BaseURL.join(self.base_url, url)

class AdaptationSet(MPDNode):
    __tag__ = u"AdaptationSet"

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        super(AdaptationSet, self).__init__(node, root, parent, *args, **kwargs)

        self.id = self.attr(u"id")
        self.group = self.attr(u"group")
        self.mimeType = self.attr(u"mimeType")
        self.lang = self.attr(u"lang")
        self.contentType = self.attr(u"contentType")
        self.par = self.attr(u"par")
        self.minBandwidth = self.attr(u"minBandwidth")
        self.maxBandwidth = self.attr(u"maxBandwidth")
        self.minWidth = self.attr(u"minWidth", parser=int)
        self.maxWidth = self.attr(u"maxWidth", parser=int)
        self.minHeight = self.attr(u"minHeight", parser=int)
        self.maxHeight = self.attr(u"maxHeight", parser=int)
        self.minFrameRate = self.attr(u"minFrameRate", parser=MPDParsers.frame_rate)
        self.maxFrameRate = self.attr(u"maxFrameRate", parser=MPDParsers.frame_rate)
        self.segmentAlignment = self.attr(u"segmentAlignment", default=False, parser=MPDParsers.bool_str)
        self.bitstreamSwitching = self.attr(u"bitstreamSwitching", parser=MPDParsers.bool_str)
        self.subsegmentAlignment = self.attr(u"subsegmentAlignment", default=False, parser=MPDParsers.bool_str)
        self.subsegmentStartsWithSAP = self.attr(u"subsegmentStartsWithSAP", default=0, parser=int)

        self.baseURLs = self.children(BaseURL)
        self.segmentTemplate = self.only_child(SegmentTemplate)
        self.representations = self.children(Representation, minimum=1)
        self.contentProtection = self.children(ContentProtection)

class SegmentTemplate(MPDNode):
    __tag__ = "SegmentTemplate"

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        super(SegmentTemplate, self).__init__(node, root, parent, *args, **kwargs)
        self.defaultSegmentTemplate = self.walk_back_get_attr('segmentTemplate')

        self.initialization = self.attr(u"initialization", parser=MPDParsers.segment_template)
        self.media = self.attr(u"media", parser=MPDParsers.segment_template)
        self.duration = self.attr(u"duration", parser=int,
                                  default=self.defaultSegmentTemplate.duration if self.defaultSegmentTemplate else None)
        self.timescale = self.attr(u"timescale", parser=int,
                                   default=self.defaultSegmentTemplate.timescale if self.defaultSegmentTemplate else 1)
        self.startNumber = self.attr(u"startNumber", parser=int,
                                     default=self.defaultSegmentTemplate.startNumber if self.defaultSegmentTemplate else 1)
        self.presentationTimeOffset = self.attr(u"presentationTimeOffset", parser=MPDParsers.timedelta(self.timescale))

        if self.duration:
            self.duration_seconds = self.duration / float(self.timescale)
        else:
            self.duration_seconds = None

        self.period = list(self.walk_back(Period))[0]

        # children
        self.segmentTimeline = self.only_child(SegmentTimeline)

    def segments(self, **kwargs):
        if kwargs.pop("init", True):
            init_url = self.format_initialization(**kwargs)
            if init_url:
                yield Segment(init_url, 0, True, False)
        for media_url, available_at in self.format_media(**kwargs):
            yield Segment(media_url, self.duration_seconds, False, True, available_at)

    def make_url(self, url):
        """
        Join the URL with the base URL, unless it's an absolute URL
        :param url: maybe relative URL
        :return: joined URL
        """
        return BaseURL.join(self.base_url, url)

    def format_initialization(self, **kwargs):
        if self.initialization:
            return self.make_url(self.initialization(**kwargs))

    def segment_numbers(self):
        """
        yield the segment number and when it will be available
        There are two cases for segment number generation, static and dynamic.

        In the case of static stream, the segment number starts at the startNumber and counts
        up to the number of segments that are represented by the periods duration.

        In the case of dynamic streams, the segments should appear at the specified time
        in the simplest case the segment number is based on the time since the availabilityStartTime
        :return:
        """
        log.debug("Generating segment numbers for {0} playlist (id={1})".format(self.root.type, self.parent.id))
        if self.root.type == u"static":
            available_iter = repeat(epoch_start)
            duration = self.period.duration.seconds or self.root.mediaPresentationDuration.seconds
            if duration:
                number_iter = range(self.startNumber, int(duration / self.duration_seconds) + 1)
            else:
                number_iter = count(self.startNumber)
        else:
            now = datetime.datetime.now(utc)
            if self.presentationTimeOffset:
                since_start = (now - self.presentationTimeOffset) - self.root.availabilityStartTime
                available_start_date = self.root.availabilityStartTime + self.presentationTimeOffset + since_start
                available_start = available_start_date
            else:
                since_start = now - self.root.availabilityStartTime
                available_start = now

            # if there is no delay, use a delay of 3 seconds
            suggested_delay = datetime.timedelta(seconds=(self.root.suggestedPresentationDelay.total_seconds()
                                                          if self.root.suggestedPresentationDelay
                                                          else 3))

            # the number of the segment that is available at NOW - SUGGESTED_DELAY - BUFFER_TIME
            number_iter = count(self.startNumber +
                                int((since_start - suggested_delay - self.root.minBufferTime).total_seconds() /
                                    self.duration_seconds))

            # the time the segment number is available at NOW
            available_iter = count_dt(available_start,
                                      step=datetime.timedelta(seconds=self.duration_seconds))

        for number, available_at in izip(number_iter, available_iter):
            yield number, available_at

    def format_media(self, **kwargs):
        if self.segmentTimeline:
            if self.parent.id is None:
                # workaround for invalid `self.root.timelines[self.parent.id]`
                # creates a timeline for every mimeType instead of one for both
                self.parent.id = self.parent.mimeType
            log.debug("Generating segment timeline for {0} playlist (id={1}))".format(self.root.type, self.parent.id))
            if self.root.type == "dynamic":
                # if there is no delay, use a delay of 3 seconds
                suggested_delay = datetime.timedelta(seconds=(self.root.suggestedPresentationDelay.total_seconds()
                                                              if self.root.suggestedPresentationDelay
                                                              else 3))
                publish_time = self.root.publishTime or epoch_start

                # transform the time line in to a segment list
                timeline = []
                available_at = publish_time
                for segment, n in reversed(list(zip(self.segmentTimeline.segments, count(self.startNumber)))):
                    # the last segment in the timeline is the most recent
                    # so, work backwards and calculate when each of the segments was
                    # available, based on the durations relative to the publish time
                    url = self.make_url(self.media(Time=segment.t, Number=n, **kwargs))
                    duration = datetime.timedelta(seconds=segment.d / self.timescale)

                    # once the suggested_delay is reach stop
                    if self.root.timelines[self.parent.id] == -1 and publish_time - available_at >= suggested_delay:
                        break

                    timeline.append((url, available_at, segment.t))

                    available_at -= duration  # walk backwards in time

                # return the segments in chronological order
                for url, available_at, t in reversed(timeline):
                    if t > self.root.timelines[self.parent.id]:
                        self.root.timelines[self.parent.id] = t
                        yield (url, available_at)

            else:
                for segment, n in zip(self.segmentTimeline.segments, count(self.startNumber)):
                    yield (self.make_url(self.media(Time=segment.t, Number=n, **kwargs)),
                           datetime.datetime.now(tz=utc))

        else:
            for number, available_at in self.segment_numbers():
                yield (self.make_url(self.media(Number=number, **kwargs)),
                       available_at)

class Representation(MPDNode):
    __tag__ = u"Representation"

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        super(Representation, self).__init__(node, root, parent, *args, **kwargs)
        self.id = self.attr(u"id", required=True)
        self.bandwidth = self.attr(u"bandwidth", parser=lambda b: float(b) / 1000.0, required=True)
        self.mimeType = self.attr(u"mimeType", required=True, inherited=True)

        self.codecs = self.attr(u"codecs")
        self.startWithSAP = self.attr(u"startWithSAP")

        # video
        self.width = self.attr(u"width", parser=int)
        self.height = self.attr(u"height", parser=int)
        self.frameRate = self.attr(u"frameRate", parser=MPDParsers.frame_rate)

        # audio
        self.audioSamplingRate = self.attr(u"audioSamplingRate", parser=int)
        self.numChannels = self.attr(u"numChannels", parser=int)

        # subtitle
        self.lang = self.attr(u"lang", inherited=True)

        self.baseURLs = self.children(BaseURL)
        self.subRepresentation = self.children(SubRepresentation)
        self.segmentBase = self.only_child(SegmentBase)
        self.segmentList = self.children(SegmentList)
        self.segmentTemplate = self.only_child(SegmentTemplate)

    @property
    def bandwidth_rounded(self):
        return round(self.bandwidth, 1 - int(math.log10(self.bandwidth)))

    def segments(self, **kwargs):
        """
        Segments are yielded when they are available

        Segments appear on a time line, for dynamic content they are only available at a certain time
        and sometimes for a limited time. For static content they are all available at the same time.

        :param kwargs: extra args to pass to the segment template
        :return: yields Segments
        """

        segmentBase = self.segmentBase or self.walk_back_get_attr("segmentBase")
        segmentLists = self.segmentList or self.walk_back_get_attr("segmentList")
        segmentTemplate = self.segmentTemplate or self.walk_back_get_attr("segmentTemplate")

        if segmentTemplate:
            for segment in segmentTemplate.segments(RepresentationID=self.id,
                                                    Bandwidth=int(self.bandwidth * 1000),
                                                    **kwargs):
                if segment.init:
                    yield segment
                else:
                    yield segment
        elif segmentLists:
            for segmentList in segmentLists:
                for segment in segmentList.segments:
                    yield segment
        else:
            yield Segment(self.base_url, 0, True, True)

class SubRepresentation(MPDNode):
    __tag__ = "SubRepresentation"

class SegmentTimeline(MPDNode):
    __tag__ = "SegmentTimeline"
    TimelineSegment = namedtuple("TimelineSegment", "t d")

    def __init__(self, node, *args, **kwargs):
        super(SegmentTimeline, self).__init__(node, *args, **kwargs)

        self.timescale = self.walk_back_get_attr("timescale")

        self.timeline_segments = self.children(_TimelineSegment)

    @property
    def segments(self):
        t = 0
        for tsegment in self.timeline_segments:
            if t == 0 and tsegment.t is not None:
                t = tsegment.t
            # check the start time from MPD
            for repeated_i in range(tsegment.r + 1):
                yield self.TimelineSegment(t, tsegment.d)
                t += tsegment.d

class _TimelineSegment(MPDNode):
    __tag__ = "S"

    def __init__(self, node, *args, **kwargs):
        super(_TimelineSegment, self).__init__(node, *args, **kwargs)

        self.t = self.attr("t", parser=int)
        self.d = self.attr("d", parser=int)
        self.r = self.attr("r", parser=int, default=0)

class ContentProtection(MPDNode):
    __tag__ = "ContentProtection"

    def __init__(self, node, root=None, parent=None, *args, **kwargs):
        super(ContentProtection, self).__init__(node, root, parent, *args, **kwargs)

        self.schemeIdUri = self.attr(u"schemeIdUri")
        self.value = self.attr(u"value")
        self.default_KID = self.attr(u"default_KID")


# Source: http://code.activestate.com/recipes/576693/

# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
# Passes Python2.7's test suite and incorporates all the latest updates.

try:
    from thread import get_ident as _get_ident
except ImportError:
    from dummy_thread import get_ident as _get_ident

try:
    from _abcoll import KeysView, ValuesView, ItemsView
except ImportError:
    pass

class OrderedDict(dict):
    'Dictionary that remembers insertion order'
    # An inherited dict maps keys to values.
    # The inherited dict provides __getitem__, __len__, __contains__, and get.
    # The remaining methods are order-aware.
    # Big-O running times for all methods are the same as for regular dictionaries.

    # The internal self.__map dictionary maps keys to links in a doubly linked list.
    # The circular doubly linked list starts and ends with a sentinel element.
    # The sentinel element never gets deleted (this simplifies the algorithm).
    # Each link is stored as a list of length three:  [PREV, NEXT, KEY].

    def __init__(self, *args, **kwds):
        '''Initialize an ordered dictionary.  Signature is the same as for
        regular dictionaries, but keyword arguments are not recommended
        because their insertion order is arbitrary.

        '''
        if len(args) > 1:
            raise TypeError('expected at most 1 arguments, got %d' % len(args))
        try:
            self.__root
        except AttributeError:
            self.__root = root = []                     # sentinel node
            root[:] = [root, root, None]
            self.__map = {}
        self.__update(*args, **kwds)

    def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
        'od.__setitem__(i, y) <==> od[i]=y'
        # Setting a new item creates a new link which goes at the end of the linked
        # list, and the inherited dictionary is updated with the new key/value pair.
        if key not in self:
            root = self.__root
            last = root[0]
            last[1] = root[0] = self.__map[key] = [last, root, key]
        dict_setitem(self, key, value)

    def __delitem__(self, key, dict_delitem=dict.__delitem__):
        'od.__delitem__(y) <==> del od[y]'
        # Deleting an existing item uses self.__map to find the link which is
        # then removed by updating the links in the predecessor and successor nodes.
        dict_delitem(self, key)
        link_prev, link_next, key = self.__map.pop(key)
        link_prev[1] = link_next
        link_next[0] = link_prev

    def __iter__(self):
        'od.__iter__() <==> iter(od)'
        root = self.__root
        curr = root[1]
        while curr is not root:
            yield curr[2]
            curr = curr[1]

    def __reversed__(self):
        'od.__reversed__() <==> reversed(od)'
        root = self.__root
        curr = root[0]
        while curr is not root:
            yield curr[2]
            curr = curr[0]

    def clear(self):
        'od.clear() -> None.  Remove all items from od.'
        try:
            for node in self.__map.itervalues():
                del node[:]
            root = self.__root
            root[:] = [root, root, None]
            self.__map.clear()
        except AttributeError:
            pass
        dict.clear(self)

    def popitem(self, last=True):
        '''od.popitem() -> (k, v), return and remove a (key, value) pair.
        Pairs are returned in LIFO order if last is true or FIFO order if false.

        '''
        if not self:
            raise KeyError('dictionary is empty')
        root = self.__root
        if last:
            link = root[0]
            link_prev = link[0]
            link_prev[1] = root
            root[0] = link_prev
        else:
            link = root[1]
            link_next = link[1]
            root[1] = link_next
            link_next[0] = root
        key = link[2]
        del self.__map[key]
        value = dict.pop(self, key)
        return key, value

    # -- the following methods do not depend on the internal structure --

    def keys(self):
        'od.keys() -> list of keys in od'
        return list(self)

    def values(self):
        'od.values() -> list of values in od'
        return [self[key] for key in self]

    def items(self):
        'od.items() -> list of (key, value) pairs in od'
        return [(key, self[key]) for key in self]

    def iterkeys(self):
        'od.iterkeys() -> an iterator over the keys in od'
        return iter(self)

    def itervalues(self):
        'od.itervalues -> an iterator over the values in od'
        for k in self:
            yield self[k]

    def iteritems(self):
        'od.iteritems -> an iterator over the (key, value) items in od'
        for k in self:
            yield (k, self[k])

    def update(*args, **kwds):
        '''od.update(E, **F) -> None.  Update od from dict/iterable E and F.

        If E is a dict instance, does:           for k in E: od[k] = E[k]
        If E has a .keys() method, does:         for k in E.keys(): od[k] = E[k]
        Or if E is an iterable of items, does:   for k, v in E: od[k] = v
        In either case, this is followed by:     for k, v in F.items(): od[k] = v

        '''
        if len(args) > 2:
            raise TypeError('update() takes at most 2 positional '
                            'arguments (%d given)' % (len(args),))
        elif not args:
            raise TypeError('update() takes at least 1 argument (0 given)')
        self = args[0]
        # Make progressively weaker assumptions about "other"
        other = ()
        if len(args) == 2:
            other = args[1]
        if isinstance(other, dict):
            for key in other:
                self[key] = other[key]
        elif hasattr(other, 'keys'):
            for key in other.keys():
                self[key] = other[key]
        else:
            for key, value in other:
                self[key] = value
        for key, value in kwds.items():
            self[key] = value

    __update = update  # let subclasses override update without breaking __init__

    __marker = object()

    def pop(self, key, default=__marker):
        '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
        If key is not found, d is returned if given, otherwise KeyError is raised.

        '''
        if key in self:
            result = self[key]
            del self[key]
            return result
        if default is self.__marker:
            raise KeyError(key)
        return default

    def setdefault(self, key, default=None):
        'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
        if key in self:
            return self[key]
        self[key] = default
        return default

    def __repr__(self, _repr_running={}):
        'od.__repr__() <==> repr(od)'
        call_key = id(self), _get_ident()
        if call_key in _repr_running:
            return '...'
        _repr_running[call_key] = 1
        try:
            if not self:
                return '%s()' % (self.__class__.__name__,)
            return '%s(%r)' % (self.__class__.__name__, self.items())
        finally:
            del _repr_running[call_key]

    def __reduce__(self):
        'Return state information for pickling'
        items = [[k, self[k]] for k in self]
        inst_dict = vars(self).copy()
        for k in vars(OrderedDict()):
            inst_dict.pop(k, None)
        if inst_dict:
            return (self.__class__, (items,), inst_dict)
        return self.__class__, (items,)

    def copy(self):
        'od.copy() -> a shallow copy of od'
        return self.__class__(self)

    @classmethod
    def fromkeys(cls, iterable, value=None):
        '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
        and values equal to v (which defaults to None).

        '''
        d = cls()
        for key in iterable:
            d[key] = value
        return d

    def __eq__(self, other):
        '''od.__eq__(y) <==> od==y.  Comparison to another OD is order-sensitive
        while comparison to a regular mapping is order-insensitive.

        '''
        if isinstance(other, OrderedDict):
            return len(self) == len(other) and self.items() == other.items()
        return dict.__eq__(self, other)

    def __ne__(self, other):
        return not self == other

    # -- the following methods are only used in Python 2.7 --

    def viewkeys(self):
        "od.viewkeys() -> a set-like object providing a view on od's keys"
        return KeysView(self)

    def viewvalues(self):
        "od.viewvalues() -> an object providing a view on od's values"
        return ValuesView(self)

    def viewitems(self):
        "od.viewitems() -> a set-like object providing a view on od's items"
        return ItemsView(self)


# -*- coding: utf-8 -*-
import re

from streamlink.plugin import Plugin
from streamlink.plugin.api import validate, useragents
from streamlink.stream import HLSStream, RTMPStream

_url_re = re.compile(r'''^https?://
    (?:\w*.)?
    showroom-live.com/
    (?:
        (?P<room_title>[\w-]+$)
        |
        room/profile\?room_id=(?P<room_id>\d+)$
    )
''', re.VERBOSE)

_room_id_re = re.compile(r'"roomId":(?P<room_id>\d+),')
_room_id_alt_re = re.compile(r'content="showroom:///room\?room_id=(?P<room_id>\d+)"')
_room_id_lookup_failure_log = 'Failed to find room_id for {0} using {1} regex'

_api_status_url = 'https://www.showroom-live.com/room/is_live?room_id={room_id}'
_api_stream_url = 'https://www.showroom-live.com/api/live/streaming_url?room_id={room_id}'

_api_stream_schema = validate.Schema(
    validate.any({
        "streaming_url_list": validate.all([
            {
                "url": validate.text,
                validate.optional("stream_name"): validate.text,
                "id": int,
                "label": validate.text,
                "is_default": int,
                "type": validate.text,
                "quality": int,
            }
        ])
    },
        {}
    )
)

# the "low latency" streams are rtmp, the others are hls
_rtmp_quality_lookup = {
    "オリジナル画質": "high",
    "オリジナル画質(低遅延)": "high",
    "original spec(low latency)": "high",
    "original spec": "high",
    "低画質": "low",
    "低画質(低遅延)": "low",
    "low spec(low latency)": "low",
    "low spec": "low"
}
# changes here must also be updated in test_plugin_showroom
_quality_weights = {
    "high": 720,
    "other": 360,
    "low": 160
}
# pages that definitely aren't rooms
_info_pages = set((
    "onlive",
    "campaign",
    "timetable",
    "event",
    "news",
    "article",
    "ranking",
    "follow",
    "search",
    "mypage",
    "payment",
    "user",
    "notice",
    "s",
    "organizer_registration",
    "lottery"
))

class Showroom(Plugin):
    @classmethod
    def can_handle_url(cls, url):
        match = _url_re.match(url)
        if not match or match.group("room_title") in _info_pages:
            return False
        return True

    @classmethod
    def stream_weight(cls, stream):
        if stream in _quality_weights:
            return _quality_weights.get(stream), "quality"

        return Plugin.stream_weight(stream)

    def __init__(self, url):
        Plugin.__init__(self, url)
        self._headers = {
            'Referer': self.url,
            'User-Agent': useragents.FIREFOX
        }
        self._room_id = None
        self._stream_urls = None

    @property
    def room_id(self):
        if self._room_id is None:
            self._room_id = self._get_room_id()
        return self._room_id

    def _get_room_id(self):
        """
        Locates unique identifier ("room_id") for the room.

        Returns the room_id as a string, or None if no room_id was found
        """
        match_dict = _url_re.match(self.url).groupdict()

        if match_dict['room_id'] is not None:
            return match_dict['room_id']
        else:
            res = self.session.http.get(self.url, headers=self._headers)
            match = _room_id_re.search(res.text)
            if not match:
                title = self.url.rsplit('/', 1)[-1]
                self.logger.debug(_room_id_lookup_failure_log.format(title, 'primary'))
                match = _room_id_alt_re.search(res.text)
                if not match:
                    self.logger.debug(_room_id_lookup_failure_log.format(title, 'secondary'))
                    return  # Raise exception?
            return match.group('room_id')

    def _get_stream_info(self, room_id):
        res = self.session.http.get(_api_stream_url.format(room_id=room_id), headers=self._headers)
        return self.session.http.json(res, schema=_api_stream_schema)

    def _get_rtmp_stream(self, stream_info):
        rtmp_url = '/'.join((stream_info['url'], stream_info['stream_name']))
        quality = _rtmp_quality_lookup.get(stream_info['label'], "other")

        params = dict(rtmp=rtmp_url, live=True)
        return quality, RTMPStream(self.session, params=params)

    def _get_streams(self):
        info = self._get_stream_info(self.room_id)
        if not info:
            return

        for stream_info in info.get("streaming_url_list", []):
            if stream_info["type"] == "rtmp":
                yield self._get_rtmp_stream(stream_info)
            elif stream_info["type"] == "hls":
                for s in HLSStream.parse_variant_playlist(self.session, stream_info["url"]).items():
                    yield s

__plugin__ = Showroom