{"action":"created","comment":{"url":"https://api.github.com/repos/Flexget/Flexget/pulls/comments/93016635","pull_request_review_id":13536474,"id":93016635,"diff_hunk":"@@ -1,125 +1,655 @@\n from __future__ import unicode_literals, division, absolute_import\n from builtins import *  # noqa pylint: disable=unused-import, redefined-builtin\n-from past.utils import old_div\n-from future.moves.urllib.parse import urlencode, quote\n+from future.moves.urllib.parse import quote_plus\n \n+import re\n import logging\n+import feedparser\n+from time import mktime\n+from xml.dom import minidom\n+from datetime import datetime, timedelta, date, time\n+\n+from requests import RequestException\n \n from flexget import plugin\n from flexget.entry import Entry\n from flexget.event import event\n-import feedparser\n+from flexget.config_schema import one_or_more\n+from flexget.utils.tools import parse_timedelta, str_to_boolean, split_title_year, str_to_naive_utc, str_to_number\n+from flexget.utils.search import normalize_unicode\n+from flexget.utils.template import RenderError\n \n-__author__ = 'deksan'\n+__author__ = 'andy (org. by deksan)'\n \n log = logging.getLogger('newznab')\n \n+CATEGORIES = {\n+    # Games Consoles\n+    'game': 1000,\n+    'game/nds': 1010,\n+    'game/psp': 1020,\n+    'game/wii': 1030,\n+    'game/xbox': 1040,\n+    'game/xbox360': 1050,\n+    'game/wiiware': 1060,\n+    'game/xbox360dlc': 1070,\n+    'game/ps3': 1080,\n+    'game/xbox-one': 1090,  # unofficial\n+    'game/ps4': 1100,  # unofficial\n+    # Movie\n+    'movie': 2000,\n+    'movie/foreign': 2010,\n+    'movie/other': 2020,\n+    'movie/sd': 2030,\n+    'movie/hd': 2040,\n+    'movie/uhd': 2045,  # unofficial\n+    'movie/bluray': 2050,\n+    'movie/3d': 2060,\n+    'movie/dvd': 2070,\n+    'movie/web-dl': 2080,  # unofficial\n+    # Music\n+    'music': 3000,\n+    'music/mp3': 3010,\n+    'music/video': 3020,\n+    'music/audiobook': 3030,\n+    'music/lossless': 3040,\n+    'music/foreign': 3060,  # unofficial\n+    # PC (Games + Software)\n+    'pc': 4000,\n+    'pc/0day': 4010,\n+    'pc/iso': 4020,\n+    'pc/mac': 4030,\n+    'pc/mobile-other': 4040,\n+    'pc/games': 4050,\n+    'pc/mobile-ios': 4060,\n+    'pc/mobile-android': 4070,\n+    # TV\n+    'tv': 5000,\n+    'tv/web-dl': 5010,  # unofficial\n+    'tv/foreign': 5020,\n+    'tv/sd': 5030,\n+    'tv/hd': 5040,\n+    'tv/uhd': 5045,  # unofficial\n+    'tv/other': 5050,\n+    'tv/sport': 5060,\n+    'tv/anime': 5070,\n+    'tv/documentary': 5080,  # unofficial\n+    # XXX\n+    'xxx': 6000,\n+    'xxx/dvd': 6010,\n+    'xxx/wmv': 6020,\n+    'xxx/xvid': 6030,\n+    'xxx/x264': 6040,\n+    'xxx/uhd': 6045,  # unofficial\n+    'xxx/pack': 6050,\n+    'xxx/imageset': 6060,\n+    'xxx/packs': 6070,  # unofficial\n+    'xxx/sd': 6080,  # unofficial\n+    'xxx/web-dl': 6090,  # unofficial\n+    # Misc\n+    'misc': 7000,\n+    'misc/misc': 7010,\n+    'misc/ebook': 7020,\n+    'misc/comics': 7030,\n+    # Unknown\n+    'not-determined': 7900\n+}\n+\n+# NOTE: all lowercase only\n+NAMESPACE_ATTRIBUTE_MAP = {\n+    'grabs': int,\n+    'size': int,\n+    'files': int,\n+    'usenetdate': datetime,\n+    'password': bool,\n+    'guid': str,\n+    'hydraindexername': str,\n+    'hydraindexerhost': str,\n+    'hydraindexerscore': int\n+}\n+\n+PLUGIN_LOOKUP_MAP = {\n+    'tvdb': 'thetvdb_lookup',\n+    'trakt': 'trakt_lookup',\n+    'tvmaze': 'tvmaze_lookup',\n+    'imdb': 'imdb_lookup',\n+    'tmdb': 'tmdb_lookup'\n+}\n+\n+NAMESPACE_NAME = 'newznab'\n+NAMESPACE_URL = 'http://www.newznab.com/DTD/2010/feeds/attributes/'\n+NAMESPACE_TAGNAME = 'attr'\n+NAMESPACE_PREFIX = 'newznab_'\n+ENCLOSURE_TYPE = 'application/x-nzb'\n+\n+\n+# utils\n+def list_insert_unique(in_list, idx, element, caseinsensitive=False):\n+    if not isinstance(in_list, list) or element is None:\n+        return\n+    if isinstance(element, str):\n+        if element.isspace() or element in in_list:\n+            return\n+        elif caseinsensitive is True and element.lower() in in_list:\n+            return\n+    elif element in in_list:\n+        return\n+    in_list.insert(idx, element)\n+\n+\n+def list_append_unique(in_list, element, caseinsensitive=False):\n+    list_insert_unique(in_list, len(in_list), element, caseinsensitive=caseinsensitive)\n+\n+\n+def list_combine_unique(list_dst, list_source, caseinsensitive=False):\n+    if not isinstance(list_dst, list) or not isinstance(list_source, list):\n+        log.error('Cant combine lists, src/dst is not a listtype: %s, %s', list_dst, list_source)\n+    for item in list_source:\n+        list_append_unique(list_dst, item, caseinsensitive=caseinsensitive)\n+\n+\n+def _debug_dump_entry(entry, types=None):\n+    if types and not isinstance(types, list):\n+        types = [types]\n+\n+    log.verbose('#####################################################################################')\n+    for key in entry:\n+        if types:\n+            for the_type in types:\n+                if isinstance(entry[key], the_type):\n+                    log.verbose('Entry: [%-30s] = %s', key, entry[key])\n+                    break\n+        else:\n+            log.verbose('Entry: [%-30s] = %s', key, entry[key])\n+\n \n class Newznab(object):\n     \"\"\"\n     Newznab search plugin\n-    Provide a url or your website + apikey and a category\n+    Provide the 'api_server_url' + 'api_key' and one or more search categories via 'category'\n+    Most common categories are: 'tv', 'movie', 'tv/hd', 'movie/hd'\n+    Valid meta names for 'use_metadata': 'tvdb', 'trakt', 'tvmaze', imdb', 'tmdb'\n+    TIP: Use nzbhydra to perform searches on multiple indexers with one config https://github.com/theotherp/nzbhydra\n+\n+    NOTE: will populate those newznab fields if available:\n+    'newznab_age'               - age in days of this release\n+    'newznab_pubdate'           - date the indexer added the nzb to its database (aka age)\n+    'newznab_guid'              - unique guid of this release\n+    'newznab_grabs'             - number of grabs\n+    'newznab_size'              - size in bytes of the release\n+    'newznab_files'             - number of files this release (archive) has\n+    'newznab_usenetdate'        - date the release was posted on usenet\n+    'newznab_password'          - if the release uses a password\n+    'newznab_hydraindexername'  - the name set in nzbhydra\n+    'newznab_hydraindexerhost'  - the host url used by nzbhydra\n+    'newznab_hydraindexerscore' - the priority score set by nzbhydra config for this indexer\n+\n+    Config example:\n+    # search by name: search in the 'tv' category, using existing names for the type ('title'...)\n+    newznab:\n+        api_server_url: https://api.nzbindexer.com\n+        api_key: my_apikey\n+        category: tv\n+\n+    # search by name + age: search in the 'tv' category, using existing names for the type ('title'...)\n+    # maxage: auto will try to calculate the maximum possible age for the release and use it to confine the search\n+    newznab:\n+        api_server_url: https://api.nzbindexer.com\n+        api_key: my_apikey\n+        category: tv\n+        maxage: auto\n \n-    Config example::\n+    # meta id based: try using existing metaid's (tvdb/ragetv/imdb) to search only in the tv/hd,uhd categories\n+    newznab:\n+        api_server_url: https://api.nzbindexer.com\n+        api_key: my_apikey\n+        category:\n+            - tv/hd\n+            - tv/uhd\n+        use_metadata: yes\n \n-        newznab:\n-          url: \"http://website/api?apikey=xxxxxxxxxxxxxxxxxxxxxxxxxx&t=movie&extended=1\"\n-          website: https://website\n-          apikey: xxxxxxxxxxxxxxxxxxxxxxxxxx\n-          category: movie\n+    # meta id based: try using metaid's (tvdb/ragetv/imdb) and if needed do a trakt/imdb lookup,\n+                     only in the movie/uhd category for releases that are not older than 1 week\n+    newznab:\n+        api_server_url: https://api.nzbindexer.com\n+        api_key: my_apikey\n+        category: movie/uhd\n+        use_metadata:\n+            - trakt\n+            - imdb\n+        maxage: 1 week\n \n-    Category is any of: movie, tvsearch, music, book\n+    # custom search string: builds the search string and searches in the tv/hd and a custom 5999 category\n+    newznab:\n+        api_server_url: https://api.nzbindexer.com\n+        api_key: my_apikey\n+        custom_query: \"{{trakt_series_name}} {{series_id}}\"\n+        category:\n+            - tv/hd\n+            - 5999\n+\n+    # combined (meta id + custom search string): try using metaid's (tvdb/ragetv/imdb) and if needed does a tvdb lookup,\n+                                                also builds/uses the custom search string.\n+    newznab:\n+        api_server_url: https://api.nzbindexer.com\n+        api_key: my_apikey\n+        custom_query: \"{{trakt_series_name}} {{series_id}}\"\n+        category: tv\n+        use_metadata: tvdb\n+\n+    # forces quotes around the search query, some ill behaving indexers wont correctly handle multiterm queries like: 'title s01e01'\n+    # If you notice you get results of titles that are not in the search query or wrong season/episodes, try this.\n+    # NOTE: will most likely break well behaving indexers, so use in a separate task/entry or use nzbhydra\n+    newznab:\n+        api_server_url: https://api.nzbindexer.com\n+        category: tv\n+        force_quotes: yes\n     \"\"\"\n \n     schema = {\n         'type': 'object',\n         'properties': {\n-            'category': {'type': 'string', 'enum': ['movie', 'tvsearch', 'tv', 'music', 'book']},\n-            'url': {'type': 'string', 'format': 'url'},\n-            'website': {'type': 'string', 'format': 'url'},\n-            'apikey': {'type': 'string'}\n+            'api_server_url': {'type': 'string', 'format': 'url'},\n+            'api_key': {'type': 'string'},\n+            'category': one_or_more({\n+                'oneOf': [\n+                    {'type': 'integer'},\n+                    {'type': 'string', 'enum': list(CATEGORIES)},\n+                ]}, unique_items=True),\n+            'custom_query': {'type': 'string'},\n+            'force_quotes': {'type': 'boolean', 'default': False},\n+            'maxage': {\n+                'oneOf': [\n+                    {'type': 'string', 'format': 'interval'},\n+                    {'type': 'string', 'enum': ['auto']}\n+                ]\n+            },\n+            'use_metadata': {\n+                'oneOf': [\n+                    {'type': 'boolean'},\n+                    one_or_more({'type': 'string', 'enum': list(PLUGIN_LOOKUP_MAP)}, unique_items=True)\n+                ]\n+            },\n         },\n-        'required': ['category'],\n+        'required': ['api_server_url', 'category'],\n         'additionalProperties': False\n     }\n \n-    def build_config(self, config):\n-        log.debug(type(config))\n+    def prepare_config(self, config):\n+        if 'use_metadata' in config:\n+            if 'plugins_list' not in config:\n+                if isinstance(config['use_metadata'], str):\n+                    config['use_metadata'] = [config['use_metadata']]\n+                if isinstance(config['use_metadata'], list):\n+                    plugins = []\n+                    for name in config['use_metadata']:\n+                        plugins.append(PLUGIN_LOOKUP_MAP[name])\n+                        config['plugins_list'] = plugins\n+        if 'category' in config:\n+            if 'category_string' not in config:\n+                if not isinstance(config['category'], list):\n+                    config['category'] = [config['category']]\n+                categories = config['category']\n+                # Convert named categories to its respective categories id number\n+                categories = [c if isinstance(c, int) else CATEGORIES[c] for c in categories]\n+                if len(categories) > 0:\n+                    config['category_string'] = ','.join(str(c) for c in categories)\n+        return config\n \n-        if config['category'] == 'tv':\n-            config['category'] = 'tvsearch'\n+    def _is_tv_entry(self, entry):\n+        if any(entry.get(field) for field in ['series_name', 'tvdb_id', 'tvrage_id', 'tvmaze_series_id', 'trakt_id']):\n+            return True\n+        elif any(entry.get(field) for field in ['movie_name', 'tmdb_id', 'imdb_id', 'trakt_movie_id']):\n+            return False\n+        else:\n+            return None\n \n-        if 'url' not in config:\n-            if 'apikey' in config and 'website' in config:\n-                params = {\n-                    't': config['category'],\n-                    'apikey': config['apikey'],\n-                    'extended': 1\n-                }\n-                config['url'] = config['website'] + '/api?' + urlencode(params)\n+    def _has_meta_id(self, entry):\n+        return any(entry.get(field) for field in ['tvdb_id', 'tvrage_id', 'tvmaze_series_id', 'tmdb_id', 'imdb_id',\n+                                                  'trakt_movie_id', 'trakt_id'])\n \n-        return config\n+    def _has_supported_meta_id(self, entry):\n+        if self._is_tv_entry(entry) is True:\n+            return any(entry.get(field) for field in ['tvdb_id', 'tvrage_id'])\n+        elif self._is_tv_entry(entry) is False:\n+            return any(entry.get(field) for field in ['imdb_id'])\n+        else:\n+            return False\n \n-    def fill_entries_for_url(self, url, task):\n-        entries = []\n-        log.verbose('Fetching %s' % url)\n+    def update_metadata(self, entry, config):\n+        if 'plugins_list' not in config:\n+            return entry\n \n-        try:\n-            r = task.requests.get(url)\n-        except task.requests.RequestException as e:\n-            log.error(\"Failed fetching '%s': %s\" % (url, e))\n+        show_lookup = self._is_tv_entry(entry)\n+        if show_lookup is None:\n+            log.warning('Could not determine entry type (tv/movie) for meta lookup: %s', entry)\n+            return entry\n+\n+        search_list = []\n+        if show_lookup is True:\n+            if entry.get('series_name'):\n+                title, year = split_title_year(entry['series_name'])\n+                if year:\n+                    search_list.insert(0, '%s (%s)' % (title, year))\n+                list_append_unique(search_list, title, caseinsensitive=True)\n+        elif show_lookup is False:\n+            search_list.insert(0, entry['title'])\n+            if entry.get('movie_name'):\n+                title, year = split_title_year(entry['movie_name'])\n+                if year:\n+                    list_append_unique(search_list, '%s (%s)' % (title, year), caseinsensitive=True)\n+                list_append_unique(search_list, title, caseinsensitive=True)\n+                if entry.get('search_strings'):\n+                    list_combine_unique(search_list, entry('search_strings'), caseinsensitive=True)\n+\n+        search_entry = Entry(entry)\n+        for search_string in search_list:\n+            search_string = search_string.strip()\n+            if not search_string:\n+                continue\n+            search_entry['title'] = search_string\n+            # update entry metadata\n+            if show_lookup is True:\n+                search_entry['series_name'] = search_string\n+                for plugin_name in config['plugins_list']:\n+                    if plugin_name == 'trakt_lookup':\n+                        plugin.get_plugin_by_name(plugin_name).instance.lazy_series_lookup(search_entry)\n+                        log.verbose('Doing `%s` for series: %s', plugin_name, search_string)\n+                    elif plugin_name == 'tvmaze_lookup':\n+                        plugin.get_plugin_by_name(plugin_name).instance.lazy_series_lookup(search_entry)\n+                        log.verbose('Doing `%s` for series: %s', plugin_name, search_string)\n+                    elif plugin_name == 'thetvdb_lookup':\n+                        # TODO @Andy: do we need language support?\n+                        plugin.get_plugin_by_name(plugin_name).instance.lazy_series_lookup(search_entry, 'en')\n+                        log.verbose('Doing `%s` for series: %s', plugin_name, search_string)\n+            elif show_lookup is False:\n+                for plugin_name in config['plugins_list']:\n+                    if plugin_name == 'trakt_lookup':\n+                        plugin.get_plugin_by_name('trakt_lookup').instance.lazy_movie_lookup(search_entry)\n+                        log.verbose('Doing `%s` for movie: %s', plugin_name, search_string)\n+                    elif plugin_name == 'tmdb_lookup':\n+                        plugin.get_plugin_by_name('tmdb_lookup').instance.lookup(search_entry)\n+                        log.verbose('Doing `%s` for movie: %s', plugin_name, search_string)\n+                    elif plugin_name == 'imdb_lookup':\n+                        plugin.get_plugin_by_name('imdb_lookup').instance.lookup(search_entry)\n+                        log.verbose('Doing `%s` for movie: %s', plugin_name, search_string)\n+            if self._has_meta_id(search_entry) is not None:\n+                search_entry['title'] = entry['title']  # keep org. title\n+                return search_entry\n+        return entry\n \n-        rss = feedparser.parse(r.content)\n-        log.debug(\"Raw RSS: %s\" % rss)\n+    def _build_base_url(self, entry, config):\n+        # TODO @Andy: do 't=caps' check (many indexers wont correctly handle this, nzbhydra uses 'brute force' since 0.2.169)\n+        if not config.get('api_server_url'):\n+            raise plugin.PluginError('Invalid url in config: %s', config)\n \n-        if not len(rss.entries):\n-            log.info('No results returned')\n+        api_url = config['api_server_url'] + '/api?' + '&extended=1'\n+        if config.get('api_key'):\n+            api_url += '&apikey=%s' % config['api_key']\n+        if config.get('category_string'):\n+            api_url += '&cat=%s' % config['category_string']\n+        if 'maxage' in config:\n+            maxage = None\n+            if config['maxage'] == 'auto':\n+                estimator = plugin.get_plugin_by_name('estimate_release').instance\n+                est_date = estimator.estimate(entry)\n+                if est_date:\n+                    if isinstance(est_date, date):\n+                        est_date = datetime.combine(est_date, time())\n+                    est_date = est_date - timedelta(hours=12)  # add a delta to compensate TZ\n+                    maxage = datetime.now() - est_date\n+            else:\n+                try:\n+                    maxage = parse_timedelta(config['maxage'])\n+                except ValueError as ex:\n+                    log.error('Invalid maxage given in config or estimator: %s', ex)\n \n-        for rss_entry in rss.entries:\n+            if maxage is not None:\n+                maxage = max(maxage, timedelta(days=1))  # cap to one day\n+                api_url += '&maxage=%s' % maxage.days\n+        return api_url\n+\n+    def parse_from_xml(self, xml_entries):\n+        entries = []\n+        for xml_entry in xml_entries:\n             new_entry = Entry()\n+            # skip if we have no link/title\n+            if not xml_entry.title or not xml_entry.link:\n+                continue\n+            # fill base data\n+            new_entry['title'] = xml_entry.title\n+            new_entry['url'] = xml_entry.link\n+            if xml_entry.enclosures:\n+                for link in xml_entry.enclosures:\n+                    if link.length and link.type == ENCLOSURE_TYPE:\n+                        new_entry['content_size'] = int(int(link.length) / 1024 / 1024)  # MB\n+            if 'content_size' not in new_entry or new_entry['content_size'] == 0:\n+                log.warning('Could not get valid filesize for entry: %s', xml_entry.title)\n \n-            for key in list(rss_entry.keys()):\n-                new_entry[key] = rss_entry[key]\n-            new_entry['url'] = new_entry['link']\n-            if rss_entry.enclosures:\n-                size = int(rss_entry.enclosures[0]['length'])  # B\n-                new_entry['content_size'] = old_div(size, 2 ** 20)  # MB\n+            # store some usefully data in the namespace\n+            if xml_entry.id:\n+                guid_splits = re.split('[/=]', xml_entry.id)\n+                new_entry[NAMESPACE_PREFIX + 'guid'] = guid_splits.pop()\n+            if xml_entry.published_parsed:\n+                new_entry[NAMESPACE_PREFIX + 'pubdate'] = datetime.fromtimestamp(mktime(xml_entry.published_parsed))\n+            elif xml_entry.published:\n+                parsed_date = str_to_naive_utc(xml_entry.published)\n+                if parsed_date:\n+                    new_entry[NAMESPACE_PREFIX + 'pubdate'] = parsed_date\n+            if (NAMESPACE_PREFIX + 'pubdate') in new_entry:\n+                try:\n+                    new_entry[NAMESPACE_PREFIX + 'age'] = datetime.now() - new_entry[NAMESPACE_PREFIX + 'pubdate']\n+                except ValueError as ex:\n+                    log.debug('Cant calculate Age via pubdate: %s in Entry: %s error : %s',\n+                              new_entry[NAMESPACE_PREFIX + 'pubdate'], xml_entry.title, ex)\n+\n+            # add some usefully attributes to the namespace\n+            self._fill_namespace_attributes(xml_entry, new_entry)\n             entries.append(new_entry)\n         return entries\n \n-    def search(self, task, entry, config=None):\n-        config = self.build_config(config)\n-        if config['category'] == 'movie':\n-            return self.do_search_movie(entry, task, config)\n-        elif config['category'] == 'tvsearch':\n-            return self.do_search_tvsearch(entry, task, config)\n-        else:\n-            entries = []\n-            log.warning(\"Not done yet...\")\n-            return entries\n-\n-    def do_search_tvsearch(self, arg_entry, task, config=None):\n-        log.info('Searching for %s' % (arg_entry['title']))\n-        # normally this should be used with next_series_episodes who has provided season and episodenumber\n-        if 'series_name' not in arg_entry or 'series_season' not in arg_entry or 'series_episode' not in arg_entry:\n+    def _set_ns_attribute(self, name, xml_entry, entry, in_type=str):\n+        tagname = NAMESPACE_PREFIX + name\n+        value = None\n+        if tagname in entry and entry.get(tagname) is not None:\n+            value = entry.get(tagname)  # use existing, but do type check\n+        elif tagname in xml_entry:\n+            if isinstance(xml_entry[tagname], dict) and 'value' in xml_entry[tagname]:\n+                value = xml_entry[tagname]['value']\n+            elif isinstance(xml_entry[tagname], str):\n+                value = xml_entry[tagname]\n+\n+        if value is not None:\n+            if isinstance(value, in_type):\n+                entry[tagname] = value\n+            elif in_type == int or in_type == float:\n+                number = str_to_number(value)\n+                if number is not None:\n+                    entry[tagname] = number\n+            elif in_type == datetime:\n+                date = str_to_naive_utc(value)\n+                if date is not None:\n+                    entry[tagname] = date\n+            elif in_type == bool:\n+                boolvalue = None\n+                if isinstance(value, str):\n+                    if str_to_boolean(value):\n+                        boolvalue = True\n+                if not boolvalue:\n+                    number = str_to_number(value)\n+                    if number is not None and number > 0:\n+                        boolvalue = True\n+                    else:\n+                        boolvalue = False\n+                entry[tagname] = boolvalue\n+            else:\n+                log.warning('Unsupported attribute type: %s via name: %s', in_type, tagname)\n+\n+    def _fill_namespace_attributes(self, xml_entry, entry):\n+        for key in NAMESPACE_ATTRIBUTE_MAP:\n+            self._set_ns_attribute(key, xml_entry, entry, NAMESPACE_ATTRIBUTE_MAP[key])\n+\n+    # feedparser cant handle namespace attributes with same tagname, so rename those nodes.\n+    def _make_feedparser_friendly(self, data):\n+        dom = minidom.parseString(data)\n+        items_ns = dom.getElementsByTagNameNS(NAMESPACE_URL, NAMESPACE_TAGNAME)\n+        if items_ns:\n+            for node in items_ns:\n+                if node.attributes and 'name' in node.attributes and 'value' in node.attributes:\n+                    node.tagName = NAMESPACE_NAME + ':%s' % node.attributes['name'].value\n+                    node.name = node.attributes['name'].value\n+                    node.value = node.attributes['value'].value\n+        return dom.toxml()\n+\n+    def fill_entries_for_url(self, url, task):\n+        log.verbose('Fetching %s', url)\n+        try:\n+            r = task.requests.get(url + '&o=xml', timeout=20)\n+        except RequestException as ex:\n+            raise plugin.PluginError(\"Failed fetching url: %s error: %s\" % (url, ex))\n+\n+        if r and r.status_code != 200:\n+            raise plugin.PluginError('Unable to reach indexer url: %s' % url)\n+\n+        xml_data = r.content\n+        try:\n+            xml_data = self._make_feedparser_friendly(xml_data)\n+        except Exception as ex:\n+            log.debug('Could not apply feedparser fix, trying without. Error: %s', ex)\n+\n+        try:\n+            parsed_xml = feedparser.parse(xml_data)\n+        except Exception as ex:\n+            raise plugin.PluginError('Unable to parse XML from url: %s error: %s' % (url, ex))\n+\n+        if 'error' in parsed_xml.feed:\n+            feed = parsed_xml.feed\n+            if 'code' in feed['error']:\n+                if 'description' in feed['error']:\n+                    log.error('Error code: %s detail: %s', feed['error']['code'], feed['error']['description'])\n+                else:\n+                    log.error('Error code: %s', feed['error']['code'])\n+            raise plugin.PluginError('Parsed XML is a error return for url: %s' % url)\n+\n+        if len(parsed_xml.entries) == 0:\n+            log.info('No entries returned from xml.')\n             return []\n-        if arg_entry.get('tvrage_id'):\n-            lookup = '&rid=%s' % arg_entry.get('tvrage_id')\n+\n+        entries = self.parse_from_xml(parsed_xml.entries)\n+        if len(entries) == 0:\n+            log.verbose('No entries parsed from xml.')\n+\n+        return entries\n+\n+    def _build_query_url_fragment(self, query_string, config):\n+        query_string = query_string.strip()\n+        if not query_string:\n+            log.error('Invalid/empty query_string given: %s', query_string)\n+            return None\n+        query = normalize_unicode(query_string)\n+        # query = normalize_scene(query)\n+        query = quote_plus(query.encode('utf8'))\n+        if config['force_quotes'] is True:\n+            query = \"&q=\\\"%s\\\"\" % query\n         else:\n-            lookup = '&q=%s' % quote(arg_entry['series_name'])\n-        url = config['url'] + lookup + '&season=%s&ep=%s' % (arg_entry['series_season'], arg_entry['series_episode'])\n-        return self.fill_entries_for_url(url, task)\n+            query = \"&q=%s\" % query\n+        return query\n \n-    def do_search_movie(self, arg_entry, task, config=None):\n-        entries = []\n-        log.info('Searching for %s (imdbid:%s)' % (arg_entry['title'], arg_entry['imdb_id']))\n-        # normally this should be used with emit_movie_queue who has imdbid (i guess)\n-        if 'imdb_id' not in arg_entry:\n-            return entries\n-\n-        imdb_id = arg_entry['imdb_id'].replace('tt', '')\n-        url = config['url'] + '&imdbid=' + imdb_id\n-        return self.fill_entries_for_url(url, task)\n+    def _build_metaid_url_fragment(self, entry):\n+        url_param = None\n+        # use first valid meta id\n+        if self._is_tv_entry(entry) is True:\n+            if entry.get('tvdb_id'):\n+                url_param = '&tvdbid=%s' % entry['tvdb_id']\n+            elif entry.get('tvrage_id'):\n+                url_param = '&rid=%s' % entry['tvrage_id']\n+                # lets not use those for now, rarely supported!\n+                # elif self.safe_get(entry, 'tvmaze_series_id', [basestring, int]):\n+                #    url_param = '&tvmazeid=%s' % entry['tvmaze_series_id']\n+                # elif self.safe_get(entry, 'trakt_id', [basestring, int]):\n+                #    url_param = '&traktid=%s' % entry['trakt_id']\n+        elif self._is_tv_entry(entry) is False:\n+            if entry.get('imdb_id'):\n+                imdb_str = str(entry['imdb_id'])\n+                url_param = '&imdbid=%s' % imdb_str.replace('tt', '')\n+                # tmdb_id is not supported for move lookups by indexers!\n+        return url_param\n+\n+    def _build_series_ep_season_url_fragment(self, entry):\n+        url_param = None\n+        if 'series_episode' in entry:\n+            url_param = '&ep=%s' % entry['series_episode']\n+            # TODO @Andy: test if indexer support episode only search!\n+            if 'series_season' in entry:\n+                url_param += '&season=%s' % entry['series_season']\n+        return url_param\n+\n+    def _get_custom_query_string(self, entry, task, config):\n+        if config.get('custom_query'):\n+            try:\n+                render_string = entry.render(config['custom_query'])\n+                render_string = render_string.strip()\n+                if render_string:\n+                    return render_string\n+            except RenderError as ex:\n+                log.error('Could not render custom_query string for: %s error: %s', entry['title'], ex)\n+        return None\n+\n+    # API search entry\n+    def search(self, task, entry, config):\n+        config = self.prepare_config(config)\n+\n+        # first check for custom_query\n+        custom_query_string = None\n+        if config.get('custom_query'):\n+            custom_query_string = self._get_custom_query_string(entry, task, config)\n+            if not custom_query_string:\n+                entry = self.update_metadata(entry, config)  # update metadata and try again\n+                custom_query_string = self._get_custom_query_string(entry, task, config)\n+                if not custom_query_string:\n+                    log.error('Could not build custom_query string for: %s ', entry['title'])\n+                    return []  # no fallback logic!\n+\n+        meta_url = None\n+        ep_url = None\n+        # build metadata url fragments\n+        if 'use_metadata' in config:\n+            meta_url = self._build_metaid_url_fragment(entry)\n+            if meta_url is None:\n+                entry = self.update_metadata(entry, config)\n+                meta_url = self._build_metaid_url_fragment(entry)  # try again\n+            if self._is_tv_entry(entry) is True:\n+                ep_url = self._build_series_ep_season_url_fragment(entry)\n+\n+        query_list = []\n+        # build meta and ep fragment\n+        url = self._build_base_url(entry, config)\n+        if meta_url and self._is_tv_entry(entry) is True and ep_url:\n+            url += '&t=tvsearch' + meta_url + ep_url\n+        elif meta_url and self._is_tv_entry(entry) is False:\n+            url += '&t=movie' + meta_url\n+        else:\n+            url += '&t=search'\n+            # use existing search_strings\n+            query_list = entry.get('search_strings', default=[entry['title']])\n+            list_append_unique(query_list, entry['title'], caseinsensitive=True)  # we trust external list?\n+\n+        # use custom query in all cases (not 100% well defined behaviour in metaid cases)\n+        list_insert_unique(query_list, 0, custom_query_string, caseinsensitive=True)","path":"flexget/plugins/sites/newznab.py","position":716,"original_position":716,"commit_id":"43019138394fae0bd51452875f1e5f72c9740cea","original_commit_id":"43019138394fae0bd51452875f1e5f72c9740cea","user":{"login":"paranoidi","id":504877,"avatar_url":"https://avatars.githubusercontent.com/u/504877?v=3","gravatar_id":"","url":"https://api.github.com/users/paranoidi","html_url":"https://github.com/paranoidi","followers_url":"https://api.github.com/users/paranoidi/followers","following_url":"https://api.github.com/users/paranoidi/following{/other_user}","gists_url":"https://api.github.com/users/paranoidi/gists{/gist_id}","starred_url":"https://api.github.com/users/paranoidi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/paranoidi/subscriptions","organizations_url":"https://api.github.com/users/paranoidi/orgs","repos_url":"https://api.github.com/users/paranoidi/repos","events_url":"https://api.github.com/users/paranoidi/events{/privacy}","received_events_url":"https://api.github.com/users/paranoidi/received_events","type":"User","site_admin":false},"body":"I think you can axe the one use method simply with inline:\r\n\r\n```\r\nif custom_query_string.lower() not in [v.lower() for v in query_list]:\r\n  query_list.insert(0, custom_query_string)\r\n```\r\n\r\nAlso, in python we don't usually add type information into variables (some_list, stuff_dict, etc), this is especially true if they are introduced within the scope where type is clearly visible. For method/func params the docstring is the place for this information (and type hinting in the future when we can be only py3 compatible)","created_at":"2016-12-19T11:45:32Z","updated_at":"2016-12-19T11:51:50Z","html_url":"https://github.com/Flexget/Flexget/pull/1530#discussion_r93016635","pull_request_url":"https://api.github.com/repos/Flexget/Flexget/pulls/1530","_links":{"self":{"href":"https://api.github.com/repos/Flexget/Flexget/pulls/comments/93016635"},"html":{"href":"https://github.com/Flexget/Flexget/pull/1530#discussion_r93016635"},"pull_request":{"href":"https://api.github.com/repos/Flexget/Flexget/pulls/1530"}}},"pull_request":{"url":"https://api.github.com/repos/Flexget/Flexget/pulls/1530","id":96354767,"html_url":"https://github.com/Flexget/Flexget/pull/1530","diff_url":"https://github.com/Flexget/Flexget/pull/1530.diff","patch_url":"https://github.com/Flexget/Flexget/pull/1530.patch","issue_url":"https://api.github.com/repos/Flexget/Flexget/issues/1530","number":1530,"state":"open","locked":false,"title":"rewrite of newznab plugin:","user":{"login":"Andy2244","id":5340247,"avatar_url":"https://avatars.githubusercontent.com/u/5340247?v=3","gravatar_id":"","url":"https://api.github.com/users/Andy2244","html_url":"https://github.com/Andy2244","followers_url":"https://api.github.com/users/Andy2244/followers","following_url":"https://api.github.com/users/Andy2244/following{/other_user}","gists_url":"https://api.github.com/users/Andy2244/gists{/gist_id}","starred_url":"https://api.github.com/users/Andy2244/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Andy2244/subscriptions","organizations_url":"https://api.github.com/users/Andy2244/orgs","repos_url":"https://api.github.com/users/Andy2244/repos","events_url":"https://api.github.com/users/Andy2244/events{/privacy}","received_events_url":"https://api.github.com/users/Andy2244/received_events","type":"User","site_admin":false},"body":"### Motivation for changes:\r\nBetter metadata lookup support and adding all of the usefully features the newznap api offers. Parsing/storing of advanced attributes that can be directly used by the Age/If and Sort_by plugins.\r\n\r\n### Detailed changes:\r\nAdded:\r\n- official and unofficial newznab categories via `category`\r\n- ability to build custom search query strings from existing fields via` custom_query`\r\n- automatic and manual release age support via `maxage`\r\n- support for ill behaving indexers via `force_quotes`\r\n- meta data lookup if configured\r\n- tvdb support for meta id based searches\r\n- additional stat support via 'newznab' namespace fields\r\n- support for nzbhydra extra search stats\r\n\r\nChanged:\r\n- `website `renamed to `api_server_url`\r\n- `apikey `renamed to `api_key`\r\n\r\nEntry changes:\r\n- added the following 'newznab' namespace fields if found:\r\n    `newznab_age`                     - age in days of this release\r\n    `newznab_pubdate`               - date the indexer added the nzb to its database (aka age)\r\n    `newznab_guid`                     - unique guid of this release\r\n    `newznab_grabs`                   - number of grabs\r\n    `newznab_size`                      - size in bytes of the release\r\n    `newznab_files`                      - number of files this release (archive) has\r\n    `newznab_usenetdate`          - date the release was posted on usenet\r\n    `newznab_password`             - if the release uses a password\r\n    `newznab_hydraindexername` - the name set in nzbhydra\r\n    `newznab_hydraindexerhost`  - the host url used by nzbhydra\r\n    `newznab_hydraindexerscore` - the priority score set by nzbhydra config for this indexer\r\n\r\n\r\n### Config usage if relevant (new plugin or updated schema):\r\n```\r\nNewznab search plugin\r\n    Provide the 'api_server_url' + 'api_key' and one or more search categories via 'category'\r\n    Most common categories are: 'tv', 'movie', 'tv/hd', 'movie/hd'\r\n    Valid meta names for 'use_metadata': 'tvdb', 'trakt', 'tvmaze', imdb', 'tmdb'\r\n    TIP: Use nzbhydra to perform searches on multiple indexers with one config https://github.com/theotherp/nzbhydra\r\n\r\n    NOTE: will populate those newznab fields if available:\r\n    'newznab_age'               - age in days of this release\r\n    'newznab_pubdate'           - date the indexer added the nzb to its database (aka age)\r\n    'newznab_guid'              - unique guid of this release\r\n    'newznab_grabs'             - number of grabs\r\n    'newznab_size'              - size in bytes of the release\r\n    'newznab_files'             - number of files this release (archive) has\r\n    'newznab_usenetdate'        - date the release was posted on usenet\r\n    'newznab_password'          - if the release uses a password\r\n    'newznab_hydraindexername'  - the name set in nzbhydra\r\n    'newznab_hydraindexerhost'  - the host url used by nzbhydra\r\n    'newznab_hydraindexerscore' - the priority score set by nzbhydra config for this indexer\r\n\r\n    Config example:\r\n    # search by name: search in the 'tv' category, using existing names for the type ('title'...)\r\n    newznab:\r\n        api_server_url: https://api.nzbindexer.com\r\n        api_key: my_apikey\r\n        category: tv\r\n\r\n    # search by name + age: search in the 'tv' category, using existing names for the type ('title'...)\r\n    # maxage: auto will try to calculate the maximum possible age for the release and use it to confine the search\r\n    newznab:\r\n        api_server_url: https://api.nzbindexer.com\r\n        api_key: my_apikey\r\n        category: tv\r\n        maxage: auto\r\n\r\n    # meta id based: try using existing metaid's (tvdb/ragetv/imdb) to search only in the tv/hd,uhd categories\r\n    newznab:\r\n        api_server_url: https://api.nzbindexer.com\r\n        api_key: my_apikey\r\n        category:\r\n            - tv/hd\r\n            - tv/uhd\r\n        use_metadata: yes\r\n\r\n    # meta id based: try using metaid's (tvdb/ragetv/imdb) and if needed do a trakt/imdb lookup,\r\n                     only in the movie/uhd category for releases that are not older than 1 week\r\n    newznab:\r\n        api_server_url: https://api.nzbindexer.com\r\n        api_key: my_apikey\r\n        category: movie/uhd\r\n        use_metadata:\r\n            - trakt\r\n            - imdb\r\n        maxage: 1 week\r\n\r\n    # custom search string: builds the search string and searches in the tv/hd and a custom 5999 category\r\n    newznab:\r\n        api_server_url: https://api.nzbindexer.com\r\n        api_key: my_apikey\r\n        custom_query: \"{{trakt_series_name}} {{series_id}}\"\r\n        category:\r\n            - tv/hd\r\n            - 5999\r\n\r\n    # combined (meta id + custom search string): try using metaid's (tvdb/ragetv/imdb) and if needed does a tvdb lookup,\r\n                                                also builds/uses the custom search string.\r\n    newznab:\r\n        api_server_url: https://api.nzbindexer.com\r\n        api_key: my_apikey\r\n        custom_query: \"{{trakt_series_name}} {{series_id}}\"\r\n        category: tv\r\n        use_metadata: tvdb\r\n\r\n    # forces quotes around the search query, some ill behaving indexers wont correctly handle multiterm queries like: 'title s01e01'\r\n    # If you notice you get results of titles that are not in the search query or wrong season/episodes, try this.\r\n    # NOTE: will most likely break well behaving indexers, so use in a separate task/entry or use nzbhydra\r\n    newznab:\r\n        api_server_url: https://api.nzbindexer.com\r\n        category: tv\r\n        force_quotes: yes\r\n```","created_at":"2016-12-02T21:21:12Z","updated_at":"2016-12-19T11:51:50Z","closed_at":null,"merged_at":null,"merge_commit_sha":"05c7195eafc52abe06c695b0c82a9cde32e54893","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/Flexget/Flexget/pulls/1530/commits","review_comments_url":"https://api.github.com/repos/Flexget/Flexget/pulls/1530/comments","review_comment_url":"https://api.github.com/repos/Flexget/Flexget/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Flexget/Flexget/issues/1530/comments","statuses_url":"https://api.github.com/repos/Flexget/Flexget/statuses/43019138394fae0bd51452875f1e5f72c9740cea","head":{"label":"Andy2244:develop","ref":"develop","sha":"43019138394fae0bd51452875f1e5f72c9740cea","user":{"login":"Andy2244","id":5340247,"avatar_url":"https://avatars.githubusercontent.com/u/5340247?v=3","gravatar_id":"","url":"https://api.github.com/users/Andy2244","html_url":"https://github.com/Andy2244","followers_url":"https://api.github.com/users/Andy2244/followers","following_url":"https://api.github.com/users/Andy2244/following{/other_user}","gists_url":"https://api.github.com/users/Andy2244/gists{/gist_id}","starred_url":"https://api.github.com/users/Andy2244/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Andy2244/subscriptions","organizations_url":"https://api.github.com/users/Andy2244/orgs","repos_url":"https://api.github.com/users/Andy2244/repos","events_url":"https://api.github.com/users/Andy2244/events{/privacy}","received_events_url":"https://api.github.com/users/Andy2244/received_events","type":"User","site_admin":false},"repo":{"id":75426381,"name":"Flexget","full_name":"Andy2244/Flexget","owner":{"login":"Andy2244","id":5340247,"avatar_url":"https://avatars.githubusercontent.com/u/5340247?v=3","gravatar_id":"","url":"https://api.github.com/users/Andy2244","html_url":"https://github.com/Andy2244","followers_url":"https://api.github.com/users/Andy2244/followers","following_url":"https://api.github.com/users/Andy2244/following{/other_user}","gists_url":"https://api.github.com/users/Andy2244/gists{/gist_id}","starred_url":"https://api.github.com/users/Andy2244/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Andy2244/subscriptions","organizations_url":"https://api.github.com/users/Andy2244/orgs","repos_url":"https://api.github.com/users/Andy2244/repos","events_url":"https://api.github.com/users/Andy2244/events{/privacy}","received_events_url":"https://api.github.com/users/Andy2244/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/Andy2244/Flexget","description":"flexget dev fork","fork":true,"url":"https://api.github.com/repos/Andy2244/Flexget","forks_url":"https://api.github.com/repos/Andy2244/Flexget/forks","keys_url":"https://api.github.com/repos/Andy2244/Flexget/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Andy2244/Flexget/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Andy2244/Flexget/teams","hooks_url":"https://api.github.com/repos/Andy2244/Flexget/hooks","issue_events_url":"https://api.github.com/repos/Andy2244/Flexget/issues/events{/number}","events_url":"https://api.github.com/repos/Andy2244/Flexget/events","assignees_url":"https://api.github.com/repos/Andy2244/Flexget/assignees{/user}","branches_url":"https://api.github.com/repos/Andy2244/Flexget/branches{/branch}","tags_url":"https://api.github.com/repos/Andy2244/Flexget/tags","blobs_url":"https://api.github.com/repos/Andy2244/Flexget/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Andy2244/Flexget/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Andy2244/Flexget/git/refs{/sha}","trees_url":"https://api.github.com/repos/Andy2244/Flexget/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Andy2244/Flexget/statuses/{sha}","languages_url":"https://api.github.com/repos/Andy2244/Flexget/languages","stargazers_url":"https://api.github.com/repos/Andy2244/Flexget/stargazers","contributors_url":"https://api.github.com/repos/Andy2244/Flexget/contributors","subscribers_url":"https://api.github.com/repos/Andy2244/Flexget/subscribers","subscription_url":"https://api.github.com/repos/Andy2244/Flexget/subscription","commits_url":"https://api.github.com/repos/Andy2244/Flexget/commits{/sha}","git_commits_url":"https://api.github.com/repos/Andy2244/Flexget/git/commits{/sha}","comments_url":"https://api.github.com/repos/Andy2244/Flexget/comments{/number}","issue_comment_url":"https://api.github.com/repos/Andy2244/Flexget/issues/comments{/number}","contents_url":"https://api.github.com/repos/Andy2244/Flexget/contents/{+path}","compare_url":"https://api.github.com/repos/Andy2244/Flexget/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Andy2244/Flexget/merges","archive_url":"https://api.github.com/repos/Andy2244/Flexget/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Andy2244/Flexget/downloads","issues_url":"https://api.github.com/repos/Andy2244/Flexget/issues{/number}","pulls_url":"https://api.github.com/repos/Andy2244/Flexget/pulls{/number}","milestones_url":"https://api.github.com/repos/Andy2244/Flexget/milestones{/number}","notifications_url":"https://api.github.com/repos/Andy2244/Flexget/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Andy2244/Flexget/labels{/name}","releases_url":"https://api.github.com/repos/Andy2244/Flexget/releases{/id}","deployments_url":"https://api.github.com/repos/Andy2244/Flexget/deployments","created_at":"2016-12-02T19:56:11Z","updated_at":"2016-12-13T13:12:03Z","pushed_at":"2016-12-18T13:07:59Z","git_url":"git://github.com/Andy2244/Flexget.git","ssh_url":"git@github.com:Andy2244/Flexget.git","clone_url":"https://github.com/Andy2244/Flexget.git","svn_url":"https://github.com/Andy2244/Flexget","homepage":"","size":48504,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"dev-main"}},"base":{"label":"Flexget:develop","ref":"develop","sha":"758069768539492f032d131b44d91ca4eda20783","user":{"login":"Flexget","id":682553,"avatar_url":"https://avatars.githubusercontent.com/u/682553?v=3","gravatar_id":"","url":"https://api.github.com/users/Flexget","html_url":"https://github.com/Flexget","followers_url":"https://api.github.com/users/Flexget/followers","following_url":"https://api.github.com/users/Flexget/following{/other_user}","gists_url":"https://api.github.com/users/Flexget/gists{/gist_id}","starred_url":"https://api.github.com/users/Flexget/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Flexget/subscriptions","organizations_url":"https://api.github.com/users/Flexget/orgs","repos_url":"https://api.github.com/users/Flexget/repos","events_url":"https://api.github.com/users/Flexget/events{/privacy}","received_events_url":"https://api.github.com/users/Flexget/received_events","type":"Organization","site_admin":false},"repo":{"id":1512901,"name":"Flexget","full_name":"Flexget/Flexget","owner":{"login":"Flexget","id":682553,"avatar_url":"https://avatars.githubusercontent.com/u/682553?v=3","gravatar_id":"","url":"https://api.github.com/users/Flexget","html_url":"https://github.com/Flexget","followers_url":"https://api.github.com/users/Flexget/followers","following_url":"https://api.github.com/users/Flexget/following{/other_user}","gists_url":"https://api.github.com/users/Flexget/gists{/gist_id}","starred_url":"https://api.github.com/users/Flexget/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Flexget/subscriptions","organizations_url":"https://api.github.com/users/Flexget/orgs","repos_url":"https://api.github.com/users/Flexget/repos","events_url":"https://api.github.com/users/Flexget/events{/privacy}","received_events_url":"https://api.github.com/users/Flexget/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Flexget/Flexget","description":"The official FlexGet repository","fork":false,"url":"https://api.github.com/repos/Flexget/Flexget","forks_url":"https://api.github.com/repos/Flexget/Flexget/forks","keys_url":"https://api.github.com/repos/Flexget/Flexget/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Flexget/Flexget/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Flexget/Flexget/teams","hooks_url":"https://api.github.com/repos/Flexget/Flexget/hooks","issue_events_url":"https://api.github.com/repos/Flexget/Flexget/issues/events{/number}","events_url":"https://api.github.com/repos/Flexget/Flexget/events","assignees_url":"https://api.github.com/repos/Flexget/Flexget/assignees{/user}","branches_url":"https://api.github.com/repos/Flexget/Flexget/branches{/branch}","tags_url":"https://api.github.com/repos/Flexget/Flexget/tags","blobs_url":"https://api.github.com/repos/Flexget/Flexget/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Flexget/Flexget/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Flexget/Flexget/git/refs{/sha}","trees_url":"https://api.github.com/repos/Flexget/Flexget/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Flexget/Flexget/statuses/{sha}","languages_url":"https://api.github.com/repos/Flexget/Flexget/languages","stargazers_url":"https://api.github.com/repos/Flexget/Flexget/stargazers","contributors_url":"https://api.github.com/repos/Flexget/Flexget/contributors","subscribers_url":"https://api.github.com/repos/Flexget/Flexget/subscribers","subscription_url":"https://api.github.com/repos/Flexget/Flexget/subscription","commits_url":"https://api.github.com/repos/Flexget/Flexget/commits{/sha}","git_commits_url":"https://api.github.com/repos/Flexget/Flexget/git/commits{/sha}","comments_url":"https://api.github.com/repos/Flexget/Flexget/comments{/number}","issue_comment_url":"https://api.github.com/repos/Flexget/Flexget/issues/comments{/number}","contents_url":"https://api.github.com/repos/Flexget/Flexget/contents/{+path}","compare_url":"https://api.github.com/repos/Flexget/Flexget/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Flexget/Flexget/merges","archive_url":"https://api.github.com/repos/Flexget/Flexget/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Flexget/Flexget/downloads","issues_url":"https://api.github.com/repos/Flexget/Flexget/issues{/number}","pulls_url":"https://api.github.com/repos/Flexget/Flexget/pulls{/number}","milestones_url":"https://api.github.com/repos/Flexget/Flexget/milestones{/number}","notifications_url":"https://api.github.com/repos/Flexget/Flexget/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Flexget/Flexget/labels{/name}","releases_url":"https://api.github.com/repos/Flexget/Flexget/releases{/id}","deployments_url":"https://api.github.com/repos/Flexget/Flexget/deployments","created_at":"2011-03-22T19:33:44Z","updated_at":"2016-12-15T12:25:23Z","pushed_at":"2016-12-19T07:17:30Z","git_url":"git://github.com/Flexget/Flexget.git","ssh_url":"git@github.com:Flexget/Flexget.git","clone_url":"https://github.com/Flexget/Flexget.git","svn_url":"https://github.com/Flexget/Flexget","homepage":"http://www.flexget.com","size":48404,"stargazers_count":592,"watchers_count":592,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":328,"mirror_url":null,"open_issues_count":100,"forks":328,"open_issues":100,"watchers":592,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/Flexget/Flexget/pulls/1530"},"html":{"href":"https://github.com/Flexget/Flexget/pull/1530"},"issue":{"href":"https://api.github.com/repos/Flexget/Flexget/issues/1530"},"comments":{"href":"https://api.github.com/repos/Flexget/Flexget/issues/1530/comments"},"review_comments":{"href":"https://api.github.com/repos/Flexget/Flexget/pulls/1530/comments"},"review_comment":{"href":"https://api.github.com/repos/Flexget/Flexget/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Flexget/Flexget/pulls/1530/commits"},"statuses":{"href":"https://api.github.com/repos/Flexget/Flexget/statuses/43019138394fae0bd51452875f1e5f72c9740cea"}}}}
{"actor":{"display_login":"paranoidi"}}