We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[APP SUBMITTED]: TypeError: cannot use a string pattern on a bytes-like object

### INFO
**Python Version**: `3.7.2 (default, Jan  3 2019, 02:55:40) [GCC 8.2.0]`
**Operating System**: `Linux-4.9.35-v7+-armv7l-with-debian-buster-sid`
**Locale**: `UTF-8`
**Branch**: [develop](../tree/develop)
**Database**: `44.14`
**Commit**: pymedusa/Medusa@18bd87dded99e1ecfbeae7757e226ea5510e0f96
**Link to Log**: https://gist.github.com/4421b6f5dd716b24746e97ed3008b0c4
### ERROR
<pre>
2019-02-10 19:30:40 ERROR    SNATCHQUEUE-SNATCH-526 :: [18bd87d] Snatch failed! For result: The.Office.(US).S03.1080p.WEB-DL.AAC2.0.AVC-TrollHD
Traceback (most recent call last):
  File "/home/pi/Medusa/<a href="../blob/18bd87dded99e1ecfbeae7757e226ea5510e0f96/medusa/search/queue.py#L503">medusa/search/queue.py</a>", line 503, in run
    self.success = snatch_episode(result)
  File "/home/pi/Medusa/<a href="../blob/18bd87dded99e1ecfbeae7757e226ea5510e0f96/medusa/search/core.py#L132">medusa/search/core.py</a>", line 132, in snatch_episode
    nzb_data = result.provider.download_nzb_for_post(result)
  File "/home/pi/Medusa/<a href="../blob/18bd87dded99e1ecfbeae7757e226ea5510e0f96/medusa/providers/nzb/binsearch.py#L275">medusa/providers/nzb/binsearch.py</a>", line 275, in download_nzb_for_post
    if not BinSearchProvider.nzb_check_segment.search(response.content):
TypeError: cannot use a string pattern on a bytes-like object
</pre>
---
_STAFF NOTIFIED_: @pymedusa/support @pymedusa/moderators


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `medusa/providers/nzb/binsearch.py`
Content:
```
1 # coding=utf-8
2 
3 """Provider code for Binsearch provider."""
4 
5 from __future__ import unicode_literals
6 
7 import logging
8 import re
9 from builtins import zip
10 from os.path import join
11 
12 from medusa import tv
13 from medusa.bs4_parser import BS4Parser
14 from medusa.helper.common import convert_size, sanitize_filename
15 from medusa.helpers import download_file
16 from medusa.logger.adapters.style import BraceAdapter
17 from medusa.providers.nzb.nzb_provider import NZBProvider
18 
19 from requests.compat import urljoin
20 
21 log = BraceAdapter(logging.getLogger(__name__))
22 log.logger.addHandler(logging.NullHandler())
23 
24 
25 class BinSearchProvider(NZBProvider):
26     """BinSearch Newznab provider."""
27 
28     size_regex = re.compile(r'size: (\d+\.\d+\xa0\w{2}), parts', re.I)
29     title_regex = re.compile(r'\"([^\"]+)"', re.I)
30     title_reqex_clean = re.compile(r'^[ \d_]+ (.+)')
31     title_regex_rss = re.compile(r'- \"([^\"]+)"', re.I)
32     nzb_check_segment = re.compile(r'<segment bytes="[\d]+"')
33 
34     def __init__(self):
35         """Initialize the class."""
36         super(BinSearchProvider, self).__init__('BinSearch')
37 
38         # Credentials
39         self.public = True
40 
41         # URLs
42         self.url = 'https://www.binsearch.info'
43         self.urls = {
44             'search': urljoin(self.url, 'index.php'),
45             'rss': urljoin(self.url, 'browse.php'),
46         }
47 
48         # Proper Strings
49         self.proper_strings = ['PROPER', 'REPACK', 'REAL', 'RERIP']
50 
51         # Miscellaneous Options
52 
53         # Cache
54         self.cache = tv.Cache(self, min_time=10)
55 
56     def search(self, search_strings, **kwargs):
57         """
58         Search a provider and parse the results.
59 
60         :param search_strings: A dict with mode (key) and the search value (value)
61         :returns: A list of search results (structure)
62         """
63         results = []
64         search_params = {
65             'adv_age': '',
66             'xminsize': 20,
67             'max': 250,
68         }
69         groups = [1, 2]
70 
71         for mode in search_strings:
72             log.debug('Search mode: {0}', mode)
73             # https://www.binsearch.info/browse.php?bg=alt.binaries.teevee&server=2
74             for search_string in search_strings[mode]:
75                 search_params['q'] = search_string
76                 for group in groups:
77                     # Try both 'search in the most popular groups' & 'search in the other groups' modes
78                     search_params['server'] = group
79                     if mode != 'RSS':
80                         log.debug('Search string: {search}', {'search': search_string})
81                         search_url = self.urls['search']
82                     else:
83                         search_params = {
84                             'bg': 'alt.binaries.teevee',
85                             'server': 2,
86                             'max': 50,
87                         }
88                         search_url = self.urls['rss']
89 
90                     response = self.session.get(search_url, params=search_params)
91                     if not response or not response.text:
92                         log.debug('No data returned from provider')
93                         continue
94 
95                     results += self.parse(response.text, mode)
96 
97         return results
98 
99     def parse(self, data, mode):
100         """
101         Parse search results for items.
102 
103         :param data: The raw response from a search
104         :param mode: The current mode used to search, e.g. RSS
105 
106         :return: A list of items found
107         """
108         def process_column_header(td):
109             return td.get_text(strip=True).lower()
110 
111         items = []
112 
113         with BS4Parser(data, 'html5lib') as html:
114 
115             # We need to store the post url, to be used with every result later on.
116             post_url = html.find('form', {'method': 'post'})['action']
117 
118             table = html.find('table', class_='xMenuT')
119             rows = table('tr') if table else []
120             row_offset = 1
121             if not rows or not len(rows) - row_offset:
122                 log.debug('Data returned from provider does not contain any torrents')
123                 return items
124 
125             headers = rows[0]('th')
126             # 0, 1, subject, poster, group, age
127             labels = [process_column_header(header) or idx
128                       for idx, header in enumerate(headers)]
129 
130             # Skip column headers
131             rows = rows[row_offset:]
132             for row in rows:
133                 try:
134                     col = dict(list(zip(labels, row('td'))))
135                     nzb_id_input = col[0 if mode == 'RSS' else 1].find('input')
136                     if not nzb_id_input:
137                         continue
138                     nzb_id = nzb_id_input['name']
139                     # Try and get the the article subject from the weird binsearch format
140                     title = self.clean_title(col['subject'].text, mode)
141 
142                 except AttributeError:
143                     log.debug('Parsing rows, that may not always have useful info. Skipping to next.')
144                     continue
145                 if not all([title, nzb_id]):
146                     continue
147 
148                 # Obtain the size from the 'description'
149                 size_field = BinSearchProvider.size_regex.search(col['subject'].text)
150                 if size_field:
151                     size_field = size_field.group(1)
152                 size = convert_size(size_field, sep='\xa0') or -1
153                 size = int(size)
154 
155                 download_url = urljoin(self.url, '{post_url}|nzb_id={nzb_id}'.format(post_url=post_url, nzb_id=nzb_id))
156 
157                 # For future use
158                 # detail_url = 'https://www.binsearch.info/?q={0}'.format(title)
159                 human_time = True
160                 date = col['age' if mode != 'RSS' else 'date'].get_text(strip=True).replace('-', ' ')
161                 if mode == 'RSS':
162                     human_time = False
163                 pubdate_raw = date
164                 pubdate = self.parse_pubdate(pubdate_raw, human_time=human_time)
165 
166                 item = {
167                     'title': title,
168                     'link': download_url,
169                     'size': size,
170                     'pubdate': pubdate,
171                 }
172                 if mode != 'RSS':
173                     log.debug('Found result: {0}', title)
174 
175                 items.append(item)
176 
177         return items
178 
179     @staticmethod
180     def clean_title(title, mode):
181         """
182         Clean title field, using a series of regex.
183 
184         RSS search requires different cleaning then the other searches.
185         When adding to this function, make sure you update the tests.
186         """
187         try:
188             if mode == 'RSS':
189                 title = BinSearchProvider.title_regex_rss.search(title).group(1)
190             else:
191                 title = BinSearchProvider.title_regex.search(title).group(1)
192                 if BinSearchProvider.title_reqex_clean.search(title):
193                     title = BinSearchProvider.title_reqex_clean.search(title).group(1)
194             for extension in ('.nfo', '.par2', '.rar', '.zip', '.nzb', '.part'):
195                 # Strip extensions that aren't part of the file name
196                 if title.endswith(extension):
197                     title = title[:len(title) - len(extension)]
198             return title
199         except AttributeError:
200             return None
201 
202     def download_result(self, result):
203         """
204         Download result from provider.
205 
206         This is used when a blackhole is used for sending the nzb file to the nzb client.
207         For now the url and the post data is stored as one string in the db, using a pipe (|) to separate them.
208 
209         :param result: A SearchResult object.
210         :return: The result of the nzb download (True/False).
211         """
212         if not self.login():
213             return False
214 
215         result_name = sanitize_filename(result.name)
216         filename = join(self._get_storage_dir(), result_name + '.' + self.provider_type)
217 
218         if result.url.startswith('http'):
219             self.session.headers.update({
220                 'Referer': '/'.join(result.url.split('/')[:3]) + '/'
221             })
222 
223         log.info('Downloading {result} from {provider} at {url}',
224                  {'result': result.name, 'provider': self.name, 'url': result.url})
225 
226         verify = False if self.public else None
227 
228         url, data = result.url.split('|')
229 
230         data = {
231             data.split('=')[1]: 'on',
232             'action': 'nzb',
233         }
234 
235         if download_file(url, filename, method='POST', data=data, session=self.session,
236                          headers=self.headers, verify=verify):
237 
238             if self._verify_download(filename):
239                 log.info('Saved {result} to {location}',
240                          {'result': result.name, 'location': filename})
241                 return True
242 
243         return False
244 
245     def download_nzb_for_post(self, result):
246         """
247         Download the nzb content, prior to sending it to the nzb download client.
248 
249         :param result: Nzb SearchResult object.
250         :return: The content of the nzb file if successful else None.
251         """
252         if not self.login():
253             return False
254 
255         # For now to separate the url and the post data, where splitting it with a pipe.
256         url, data = result.url.split('|')
257 
258         data = {
259             data.split('=')[1]: 'on',
260             'action': 'nzb',
261         }
262 
263         log.info('Downloading {result} from {provider} at {url} and data {data}',
264                  {'result': result.name, 'provider': self.name, 'url': result.url, 'data': data})
265 
266         verify = False if self.public else None
267 
268         response = self.session.post(url, data=data, headers=self.session.headers,
269                                      verify=verify, hooks={}, allow_redirects=True)
270         if not response or not response.content:
271             log.warning('Failed to download the NZB from BinSearch')
272             return None
273 
274         # Validate that the result has the content of a valid nzb.
275         if not BinSearchProvider.nzb_check_segment.search(response.content):
276             log.warning('Result returned from BinSearch was not a valid NZB')
277             return None
278 
279         return response.content
280 
281     def _get_size(self, item):
282         """
283         Get result size.
284 
285         Overwrite this, as the default _get_size() from nzb_provider isn't working for us.
286         :param item:
287         :return: size in bytes or -1
288         """
289         return item.get('size', -1)
290 
291 
292 provider = BinSearchProvider()
293
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/medusa/providers/nzb/binsearch.py b/medusa/providers/nzb/binsearch.py
--- a/medusa/providers/nzb/binsearch.py
+++ b/medusa/providers/nzb/binsearch.py
@@ -272,7 +272,7 @@
             return None
 
         # Validate that the result has the content of a valid nzb.
-        if not BinSearchProvider.nzb_check_segment.search(response.content):
+        if not BinSearchProvider.nzb_check_segment.search(response.text):
             log.warning('Result returned from BinSearch was not a valid NZB')
             return None

{"golden_diff": "diff --git a/medusa/providers/nzb/binsearch.py b/medusa/providers/nzb/binsearch.py\n--- a/medusa/providers/nzb/binsearch.py\n+++ b/medusa/providers/nzb/binsearch.py\n@@ -272,7 +272,7 @@\n             return None\n \n         # Validate that the result has the content of a valid nzb.\n-        if not BinSearchProvider.nzb_check_segment.search(response.content):\n+        if not BinSearchProvider.nzb_check_segment.search(response.text):\n             log.warning('Result returned from BinSearch was not a valid NZB')\n             return None\n", "issue": "[APP SUBMITTED]: TypeError: cannot use a string pattern on a bytes-like object\n\n### INFO\n**Python Version**: `3.7.2 (default, Jan  3 2019, 02:55:40) [GCC 8.2.0]`\n**Operating System**: `Linux-4.9.35-v7+-armv7l-with-debian-buster-sid`\n**Locale**: `UTF-8`\n**Branch**: [develop](../tree/develop)\n**Database**: `44.14`\n**Commit**: pymedusa/Medusa@18bd87dded99e1ecfbeae7757e226ea5510e0f96\n**Link to Log**: https://gist.github.com/4421b6f5dd716b24746e97ed3008b0c4\n### ERROR\n<pre>\n2019-02-10 19:30:40 ERROR    SNATCHQUEUE-SNATCH-526 :: [18bd87d] Snatch failed! For result: The.Office.(US).S03.1080p.WEB-DL.AAC2.0.AVC-TrollHD\nTraceback (most recent call last):\n  File \"/home/pi/Medusa/<a href=\"../blob/18bd87dded99e1ecfbeae7757e226ea5510e0f96/medusa/search/queue.py#L503\">medusa/search/queue.py</a>\", line 503, in run\n    self.success = snatch_episode(result)\n  File \"/home/pi/Medusa/<a href=\"../blob/18bd87dded99e1ecfbeae7757e226ea5510e0f96/medusa/search/core.py#L132\">medusa/search/core.py</a>\", line 132, in snatch_episode\n    nzb_data = result.provider.download_nzb_for_post(result)\n  File \"/home/pi/Medusa/<a href=\"../blob/18bd87dded99e1ecfbeae7757e226ea5510e0f96/medusa/providers/nzb/binsearch.py#L275\">medusa/providers/nzb/binsearch.py</a>\", line 275, in download_nzb_for_post\n    if not BinSearchProvider.nzb_check_segment.search(response.content):\nTypeError: cannot use a string pattern on a bytes-like object\n</pre>\n---\n_STAFF NOTIFIED_: @pymedusa/support @pymedusa/moderators\n\n", "before_files": [{"content": "# coding=utf-8\n\n\"\"\"Provider code for Binsearch provider.\"\"\"\n\nfrom __future__ import unicode_literals\n\nimport logging\nimport re\nfrom builtins import zip\nfrom os.path import join\n\nfrom medusa import tv\nfrom medusa.bs4_parser import BS4Parser\nfrom medusa.helper.common import convert_size, sanitize_filename\nfrom medusa.helpers import download_file\nfrom medusa.logger.adapters.style import BraceAdapter\nfrom medusa.providers.nzb.nzb_provider import NZBProvider\n\nfrom requests.compat import urljoin\n\nlog = BraceAdapter(logging.getLogger(__name__))\nlog.logger.addHandler(logging.NullHandler())\n\n\nclass BinSearchProvider(NZBProvider):\n    \"\"\"BinSearch Newznab provider.\"\"\"\n\n    size_regex = re.compile(r'size: (\\d+\\.\\d+\\xa0\\w{2}), parts', re.I)\n    title_regex = re.compile(r'\\\"([^\\\"]+)\"', re.I)\n    title_reqex_clean = re.compile(r'^[ \\d_]+ (.+)')\n    title_regex_rss = re.compile(r'- \\\"([^\\\"]+)\"', re.I)\n    nzb_check_segment = re.compile(r'<segment bytes=\"[\\d]+\"')\n\n    def __init__(self):\n        \"\"\"Initialize the class.\"\"\"\n        super(BinSearchProvider, self).__init__('BinSearch')\n\n        # Credentials\n        self.public = True\n\n        # URLs\n        self.url = 'https://www.binsearch.info'\n        self.urls = {\n            'search': urljoin(self.url, 'index.php'),\n            'rss': urljoin(self.url, 'browse.php'),\n        }\n\n        # Proper Strings\n        self.proper_strings = ['PROPER', 'REPACK', 'REAL', 'RERIP']\n\n        # Miscellaneous Options\n\n        # Cache\n        self.cache = tv.Cache(self, min_time=10)\n\n    def search(self, search_strings, **kwargs):\n        \"\"\"\n        Search a provider and parse the results.\n\n        :param search_strings: A dict with mode (key) and the search value (value)\n        :returns: A list of search results (structure)\n        \"\"\"\n        results = []\n        search_params = {\n            'adv_age': '',\n            'xminsize': 20,\n            'max': 250,\n        }\n        groups = [1, 2]\n\n        for mode in search_strings:\n            log.debug('Search mode: {0}', mode)\n            # https://www.binsearch.info/browse.php?bg=alt.binaries.teevee&server=2\n            for search_string in search_strings[mode]:\n                search_params['q'] = search_string\n                for group in groups:\n                    # Try both 'search in the most popular groups' & 'search in the other groups' modes\n                    search_params['server'] = group\n                    if mode != 'RSS':\n                        log.debug('Search string: {search}', {'search': search_string})\n                        search_url = self.urls['search']\n                    else:\n                        search_params = {\n                            'bg': 'alt.binaries.teevee',\n                            'server': 2,\n                            'max': 50,\n                        }\n                        search_url = self.urls['rss']\n\n                    response = self.session.get(search_url, params=search_params)\n                    if not response or not response.text:\n                        log.debug('No data returned from provider')\n                        continue\n\n                    results += self.parse(response.text, mode)\n\n        return results\n\n    def parse(self, data, mode):\n        \"\"\"\n        Parse search results for items.\n\n        :param data: The raw response from a search\n        :param mode: The current mode used to search, e.g. RSS\n\n        :return: A list of items found\n        \"\"\"\n        def process_column_header(td):\n            return td.get_text(strip=True).lower()\n\n        items = []\n\n        with BS4Parser(data, 'html5lib') as html:\n\n            # We need to store the post url, to be used with every result later on.\n            post_url = html.find('form', {'method': 'post'})['action']\n\n            table = html.find('table', class_='xMenuT')\n            rows = table('tr') if table else []\n            row_offset = 1\n            if not rows or not len(rows) - row_offset:\n                log.debug('Data returned from provider does not contain any torrents')\n                return items\n\n            headers = rows[0]('th')\n            # 0, 1, subject, poster, group, age\n            labels = [process_column_header(header) or idx\n                      for idx, header in enumerate(headers)]\n\n            # Skip column headers\n            rows = rows[row_offset:]\n            for row in rows:\n                try:\n                    col = dict(list(zip(labels, row('td'))))\n                    nzb_id_input = col[0 if mode == 'RSS' else 1].find('input')\n                    if not nzb_id_input:\n                        continue\n                    nzb_id = nzb_id_input['name']\n                    # Try and get the the article subject from the weird binsearch format\n                    title = self.clean_title(col['subject'].text, mode)\n\n                except AttributeError:\n                    log.debug('Parsing rows, that may not always have useful info. Skipping to next.')\n                    continue\n                if not all([title, nzb_id]):\n                    continue\n\n                # Obtain the size from the 'description'\n                size_field = BinSearchProvider.size_regex.search(col['subject'].text)\n                if size_field:\n                    size_field = size_field.group(1)\n                size = convert_size(size_field, sep='\\xa0') or -1\n                size = int(size)\n\n                download_url = urljoin(self.url, '{post_url}|nzb_id={nzb_id}'.format(post_url=post_url, nzb_id=nzb_id))\n\n                # For future use\n                # detail_url = 'https://www.binsearch.info/?q={0}'.format(title)\n                human_time = True\n                date = col['age' if mode != 'RSS' else 'date'].get_text(strip=True).replace('-', ' ')\n                if mode == 'RSS':\n                    human_time = False\n                pubdate_raw = date\n                pubdate = self.parse_pubdate(pubdate_raw, human_time=human_time)\n\n                item = {\n                    'title': title,\n                    'link': download_url,\n                    'size': size,\n                    'pubdate': pubdate,\n                }\n                if mode != 'RSS':\n                    log.debug('Found result: {0}', title)\n\n                items.append(item)\n\n        return items\n\n    @staticmethod\n    def clean_title(title, mode):\n        \"\"\"\n        Clean title field, using a series of regex.\n\n        RSS search requires different cleaning then the other searches.\n        When adding to this function, make sure you update the tests.\n        \"\"\"\n        try:\n            if mode == 'RSS':\n                title = BinSearchProvider.title_regex_rss.search(title).group(1)\n            else:\n                title = BinSearchProvider.title_regex.search(title).group(1)\n                if BinSearchProvider.title_reqex_clean.search(title):\n                    title = BinSearchProvider.title_reqex_clean.search(title).group(1)\n            for extension in ('.nfo', '.par2', '.rar', '.zip', '.nzb', '.part'):\n                # Strip extensions that aren't part of the file name\n                if title.endswith(extension):\n                    title = title[:len(title) - len(extension)]\n            return title\n        except AttributeError:\n            return None\n\n    def download_result(self, result):\n        \"\"\"\n        Download result from provider.\n\n        This is used when a blackhole is used for sending the nzb file to the nzb client.\n        For now the url and the post data is stored as one string in the db, using a pipe (|) to separate them.\n\n        :param result: A SearchResult object.\n        :return: The result of the nzb download (True/False).\n        \"\"\"\n        if not self.login():\n            return False\n\n        result_name = sanitize_filename(result.name)\n        filename = join(self._get_storage_dir(), result_name + '.' + self.provider_type)\n\n        if result.url.startswith('http'):\n            self.session.headers.update({\n                'Referer': '/'.join(result.url.split('/')[:3]) + '/'\n            })\n\n        log.info('Downloading {result} from {provider} at {url}',\n                 {'result': result.name, 'provider': self.name, 'url': result.url})\n\n        verify = False if self.public else None\n\n        url, data = result.url.split('|')\n\n        data = {\n            data.split('=')[1]: 'on',\n            'action': 'nzb',\n        }\n\n        if download_file(url, filename, method='POST', data=data, session=self.session,\n                         headers=self.headers, verify=verify):\n\n            if self._verify_download(filename):\n                log.info('Saved {result} to {location}',\n                         {'result': result.name, 'location': filename})\n                return True\n\n        return False\n\n    def download_nzb_for_post(self, result):\n        \"\"\"\n        Download the nzb content, prior to sending it to the nzb download client.\n\n        :param result: Nzb SearchResult object.\n        :return: The content of the nzb file if successful else None.\n        \"\"\"\n        if not self.login():\n            return False\n\n        # For now to separate the url and the post data, where splitting it with a pipe.\n        url, data = result.url.split('|')\n\n        data = {\n            data.split('=')[1]: 'on',\n            'action': 'nzb',\n        }\n\n        log.info('Downloading {result} from {provider} at {url} and data {data}',\n                 {'result': result.name, 'provider': self.name, 'url': result.url, 'data': data})\n\n        verify = False if self.public else None\n\n        response = self.session.post(url, data=data, headers=self.session.headers,\n                                     verify=verify, hooks={}, allow_redirects=True)\n        if not response or not response.content:\n            log.warning('Failed to download the NZB from BinSearch')\n            return None\n\n        # Validate that the result has the content of a valid nzb.\n        if not BinSearchProvider.nzb_check_segment.search(response.content):\n            log.warning('Result returned from BinSearch was not a valid NZB')\n            return None\n\n        return response.content\n\n    def _get_size(self, item):\n        \"\"\"\n        Get result size.\n\n        Overwrite this, as the default _get_size() from nzb_provider isn't working for us.\n        :param item:\n        :return: size in bytes or -1\n        \"\"\"\n        return item.get('size', -1)\n\n\nprovider = BinSearchProvider()\n", "path": "medusa/providers/nzb/binsearch.py"}], "after_files": [{"content": "# coding=utf-8\n\n\"\"\"Provider code for Binsearch provider.\"\"\"\n\nfrom __future__ import unicode_literals\n\nimport logging\nimport re\nfrom builtins import zip\nfrom os.path import join\n\nfrom medusa import tv\nfrom medusa.bs4_parser import BS4Parser\nfrom medusa.helper.common import convert_size, sanitize_filename\nfrom medusa.helpers import download_file\nfrom medusa.logger.adapters.style import BraceAdapter\nfrom medusa.providers.nzb.nzb_provider import NZBProvider\n\nfrom requests.compat import urljoin\n\nlog = BraceAdapter(logging.getLogger(__name__))\nlog.logger.addHandler(logging.NullHandler())\n\n\nclass BinSearchProvider(NZBProvider):\n    \"\"\"BinSearch Newznab provider.\"\"\"\n\n    size_regex = re.compile(r'size: (\\d+\\.\\d+\\xa0\\w{2}), parts', re.I)\n    title_regex = re.compile(r'\\\"([^\\\"]+)\"', re.I)\n    title_reqex_clean = re.compile(r'^[ \\d_]+ (.+)')\n    title_regex_rss = re.compile(r'- \\\"([^\\\"]+)\"', re.I)\n    nzb_check_segment = re.compile(r'<segment bytes=\"[\\d]+\"')\n\n    def __init__(self):\n        \"\"\"Initialize the class.\"\"\"\n        super(BinSearchProvider, self).__init__('BinSearch')\n\n        # Credentials\n        self.public = True\n\n        # URLs\n        self.url = 'https://www.binsearch.info'\n        self.urls = {\n            'search': urljoin(self.url, 'index.php'),\n            'rss': urljoin(self.url, 'browse.php'),\n        }\n\n        # Proper Strings\n        self.proper_strings = ['PROPER', 'REPACK', 'REAL', 'RERIP']\n\n        # Miscellaneous Options\n\n        # Cache\n        self.cache = tv.Cache(self, min_time=10)\n\n    def search(self, search_strings, **kwargs):\n        \"\"\"\n        Search a provider and parse the results.\n\n        :param search_strings: A dict with mode (key) and the search value (value)\n        :returns: A list of search results (structure)\n        \"\"\"\n        results = []\n        search_params = {\n            'adv_age': '',\n            'xminsize': 20,\n            'max': 250,\n        }\n        groups = [1, 2]\n\n        for mode in search_strings:\n            log.debug('Search mode: {0}', mode)\n            # https://www.binsearch.info/browse.php?bg=alt.binaries.teevee&server=2\n            for search_string in search_strings[mode]:\n                search_params['q'] = search_string\n                for group in groups:\n                    # Try both 'search in the most popular groups' & 'search in the other groups' modes\n                    search_params['server'] = group\n                    if mode != 'RSS':\n                        log.debug('Search string: {search}', {'search': search_string})\n                        search_url = self.urls['search']\n                    else:\n                        search_params = {\n                            'bg': 'alt.binaries.teevee',\n                            'server': 2,\n                            'max': 50,\n                        }\n                        search_url = self.urls['rss']\n\n                    response = self.session.get(search_url, params=search_params)\n                    if not response or not response.text:\n                        log.debug('No data returned from provider')\n                        continue\n\n                    results += self.parse(response.text, mode)\n\n        return results\n\n    def parse(self, data, mode):\n        \"\"\"\n        Parse search results for items.\n\n        :param data: The raw response from a search\n        :param mode: The current mode used to search, e.g. RSS\n\n        :return: A list of items found\n        \"\"\"\n        def process_column_header(td):\n            return td.get_text(strip=True).lower()\n\n        items = []\n\n        with BS4Parser(data, 'html5lib') as html:\n\n            # We need to store the post url, to be used with every result later on.\n            post_url = html.find('form', {'method': 'post'})['action']\n\n            table = html.find('table', class_='xMenuT')\n            rows = table('tr') if table else []\n            row_offset = 1\n            if not rows or not len(rows) - row_offset:\n                log.debug('Data returned from provider does not contain any torrents')\n                return items\n\n            headers = rows[0]('th')\n            # 0, 1, subject, poster, group, age\n            labels = [process_column_header(header) or idx\n                      for idx, header in enumerate(headers)]\n\n            # Skip column headers\n            rows = rows[row_offset:]\n            for row in rows:\n                try:\n                    col = dict(list(zip(labels, row('td'))))\n                    nzb_id_input = col[0 if mode == 'RSS' else 1].find('input')\n                    if not nzb_id_input:\n                        continue\n                    nzb_id = nzb_id_input['name']\n                    # Try and get the the article subject from the weird binsearch format\n                    title = self.clean_title(col['subject'].text, mode)\n\n                except AttributeError:\n                    log.debug('Parsing rows, that may not always have useful info. Skipping to next.')\n                    continue\n                if not all([title, nzb_id]):\n                    continue\n\n                # Obtain the size from the 'description'\n                size_field = BinSearchProvider.size_regex.search(col['subject'].text)\n                if size_field:\n                    size_field = size_field.group(1)\n                size = convert_size(size_field, sep='\\xa0') or -1\n                size = int(size)\n\n                download_url = urljoin(self.url, '{post_url}|nzb_id={nzb_id}'.format(post_url=post_url, nzb_id=nzb_id))\n\n                # For future use\n                # detail_url = 'https://www.binsearch.info/?q={0}'.format(title)\n                human_time = True\n                date = col['age' if mode != 'RSS' else 'date'].get_text(strip=True).replace('-', ' ')\n                if mode == 'RSS':\n                    human_time = False\n                pubdate_raw = date\n                pubdate = self.parse_pubdate(pubdate_raw, human_time=human_time)\n\n                item = {\n                    'title': title,\n                    'link': download_url,\n                    'size': size,\n                    'pubdate': pubdate,\n                }\n                if mode != 'RSS':\n                    log.debug('Found result: {0}', title)\n\n                items.append(item)\n\n        return items\n\n    @staticmethod\n    def clean_title(title, mode):\n        \"\"\"\n        Clean title field, using a series of regex.\n\n        RSS search requires different cleaning then the other searches.\n        When adding to this function, make sure you update the tests.\n        \"\"\"\n        try:\n            if mode == 'RSS':\n                title = BinSearchProvider.title_regex_rss.search(title).group(1)\n            else:\n                title = BinSearchProvider.title_regex.search(title).group(1)\n                if BinSearchProvider.title_reqex_clean.search(title):\n                    title = BinSearchProvider.title_reqex_clean.search(title).group(1)\n            for extension in ('.nfo', '.par2', '.rar', '.zip', '.nzb', '.part'):\n                # Strip extensions that aren't part of the file name\n                if title.endswith(extension):\n                    title = title[:len(title) - len(extension)]\n            return title\n        except AttributeError:\n            return None\n\n    def download_result(self, result):\n        \"\"\"\n        Download result from provider.\n\n        This is used when a blackhole is used for sending the nzb file to the nzb client.\n        For now the url and the post data is stored as one string in the db, using a pipe (|) to separate them.\n\n        :param result: A SearchResult object.\n        :return: The result of the nzb download (True/False).\n        \"\"\"\n        if not self.login():\n            return False\n\n        result_name = sanitize_filename(result.name)\n        filename = join(self._get_storage_dir(), result_name + '.' + self.provider_type)\n\n        if result.url.startswith('http'):\n            self.session.headers.update({\n                'Referer': '/'.join(result.url.split('/')[:3]) + '/'\n            })\n\n        log.info('Downloading {result} from {provider} at {url}',\n                 {'result': result.name, 'provider': self.name, 'url': result.url})\n\n        verify = False if self.public else None\n\n        url, data = result.url.split('|')\n\n        data = {\n            data.split('=')[1]: 'on',\n            'action': 'nzb',\n        }\n\n        if download_file(url, filename, method='POST', data=data, session=self.session,\n                         headers=self.headers, verify=verify):\n\n            if self._verify_download(filename):\n                log.info('Saved {result} to {location}',\n                         {'result': result.name, 'location': filename})\n                return True\n\n        return False\n\n    def download_nzb_for_post(self, result):\n        \"\"\"\n        Download the nzb content, prior to sending it to the nzb download client.\n\n        :param result: Nzb SearchResult object.\n        :return: The content of the nzb file if successful else None.\n        \"\"\"\n        if not self.login():\n            return False\n\n        # For now to separate the url and the post data, where splitting it with a pipe.\n        url, data = result.url.split('|')\n\n        data = {\n            data.split('=')[1]: 'on',\n            'action': 'nzb',\n        }\n\n        log.info('Downloading {result} from {provider} at {url} and data {data}',\n                 {'result': result.name, 'provider': self.name, 'url': result.url, 'data': data})\n\n        verify = False if self.public else None\n\n        response = self.session.post(url, data=data, headers=self.session.headers,\n                                     verify=verify, hooks={}, allow_redirects=True)\n        if not response or not response.content:\n            log.warning('Failed to download the NZB from BinSearch')\n            return None\n\n        # Validate that the result has the content of a valid nzb.\n        if not BinSearchProvider.nzb_check_segment.search(response.text):\n            log.warning('Result returned from BinSearch was not a valid NZB')\n            return None\n\n        return response.content\n\n    def _get_size(self, item):\n        \"\"\"\n        Get result size.\n\n        Overwrite this, as the default _get_size() from nzb_provider isn't working for us.\n        :param item:\n        :return: size in bytes or -1\n        \"\"\"\n        return item.get('size', -1)\n\n\nprovider = BinSearchProvider()\n", "path": "medusa/providers/nzb/binsearch.py"}]}