We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Nebulance - Cached results for manual search get duplicated
**Describe the bug**
As described in #8787 
Provider Nebulance duplicates cache results.
![image](https://user-images.githubusercontent.com/4103090/110449268-53085d80-80c2-11eb-9345-5589220a2e42.png)

If you need access to the Provider reach out to me in DM on Discord.
I can also provide the cache.db if needed.

PS: Should rarbg already be deduplicated?
![image](https://user-images.githubusercontent.com/4103090/110449560-9793f900-80c2-11eb-980f-cfd0b0a12252.png)


--- 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/torrent/html/nebulance.py`
Content:
```
1 # coding=utf-8
2 
3 """Provider code for Nebulance."""
4 
5 from __future__ import unicode_literals
6 
7 import logging
8 import re
9 
10 from medusa import tv
11 from medusa.bs4_parser import BS4Parser
12 from medusa.helper.common import (
13     convert_size,
14     try_int,
15 )
16 from medusa.helper.exceptions import AuthException
17 from medusa.logger.adapters.style import BraceAdapter
18 from medusa.providers.torrent.torrent_provider import TorrentProvider
19 
20 from requests.compat import urljoin
21 from requests.utils import dict_from_cookiejar
22 
23 log = BraceAdapter(logging.getLogger(__name__))
24 log.logger.addHandler(logging.NullHandler())
25 
26 
27 class NebulanceProvider(TorrentProvider):
28     """Nebulance Torrent provider."""
29 
30     def __init__(self):
31         """Initialize the class."""
32         super(NebulanceProvider, self).__init__('Nebulance')
33 
34         # Credentials
35         self.username = None
36         self.password = None
37 
38         # URLs
39         self.url = 'https://nebulance.io/'
40         self.urls = {
41             'login': urljoin(self.url, '/login.php'),
42             'search': urljoin(self.url, '/torrents.php'),
43         }
44 
45         # Proper Strings
46 
47         # Miscellaneous Options
48         self.freeleech = None
49 
50         # Cache
51         self.cache = tv.Cache(self)
52 
53     def search(self, search_strings, age=0, ep_obj=None, **kwargs):
54         """
55         Search a provider and parse the results.
56 
57         :param search_strings: A dict with mode (key) and the search value (value)
58         :param age: Not used
59         :param ep_obj: Not used
60         :returns: A list of search results (structure)
61         """
62         results = []
63         if not self.login():
64             return results
65 
66         for mode in search_strings:
67             log.debug('Search mode: {0}', mode)
68 
69             for search_string in search_strings[mode]:
70 
71                 if mode != 'RSS':
72                     log.debug('Search string: {search}',
73                               {'search': search_string})
74 
75                 search_params = {
76                     'searchtext': search_string,
77                     'filter_freeleech': (0, 1)[self.freeleech is True],
78                     'order_by': ('seeders', 'time')[mode == 'RSS'],
79                     'order_way': 'desc',
80                 }
81 
82                 if not search_string:
83                     del search_params['searchtext']
84 
85                 response = self.session.get(self.urls['search'], params=search_params)
86                 if not response or not response.text:
87                     log.debug('No data returned from provider')
88                     continue
89 
90                 results += self.parse(response.text, mode)
91 
92         return results
93 
94     def parse(self, data, mode):
95         """
96         Parse search results for items.
97 
98         :param data: The raw response from a search
99         :param mode: The current mode used to search, e.g. RSS
100 
101         :return: A list of items found
102         """
103         items = []
104 
105         with BS4Parser(data, 'html5lib') as html:
106             torrent_table = html.find('table', {'id': 'torrent_table'})
107 
108             # Continue only if at least one release is found
109             if not torrent_table:
110                 log.debug('Data returned from provider does not contain any torrents')
111                 return items
112 
113             torrent_rows = torrent_table('tr', {'class': 'torrent'})
114 
115             # Continue only if one Release is found
116             if not torrent_rows:
117                 log.debug('Data returned from provider does not contain any torrents')
118                 return items
119 
120             for row in torrent_rows:
121                 try:
122                     freeleech = row.find('img', alt='Freeleech') is not None
123                     if self.freeleech and not freeleech:
124                         continue
125 
126                     download_item = row.find('a', {'title': [
127                         'Download Torrent',  # Download link
128                         'Previously Grabbed Torrent File',  # Already Downloaded
129                         'Currently Seeding Torrent',  # Seeding
130                         'Currently Leeching Torrent',  # Leeching
131                     ]})
132 
133                     if not download_item:
134                         continue
135 
136                     download_url = urljoin(self.url, download_item['href'])
137 
138                     temp_anchor = row.find('a', {'data-src': True})
139                     title = temp_anchor['data-src']
140                     if not all([title, download_url]):
141                         continue
142 
143                     cells = row('td')
144                     seeders = try_int(cells[5].text.strip())
145                     leechers = try_int(cells[6].text.strip())
146 
147                     # Filter unseeded torrent
148                     if seeders < self.minseed:
149                         if mode != 'RSS':
150                             log.debug("Discarding torrent because it doesn't meet the"
151                                       ' minimum seeders: {0}. Seeders: {1}',
152                                       title, seeders)
153                         continue
154 
155                     torrent_size = cells[2].find('div').get_text(strip=True)
156                     size = convert_size(torrent_size) or -1
157 
158                     pubdate_raw = cells[3].find('span')['title']
159                     pubdate = self.parse_pubdate(pubdate_raw)
160 
161                     item = {
162                         'title': title,
163                         'link': download_url,
164                         'size': size,
165                         'seeders': seeders,
166                         'leechers': leechers,
167                         'pubdate': pubdate,
168                     }
169                     if mode != 'RSS':
170                         log.debug('Found result: {0} with {1} seeders and {2} leechers',
171                                   title, seeders, leechers)
172 
173                     items.append(item)
174                 except (AttributeError, TypeError, KeyError, ValueError, IndexError):
175                     log.exception('Failed parsing provider.')
176 
177         return items
178 
179     def login(self):
180         """Login method used for logging in before doing search and torrent downloads."""
181         if any(dict_from_cookiejar(self.session.cookies).values()):
182             return True
183 
184         login_params = {
185             'username': self.username,
186             'password': self.password,
187             'keeplogged': 'on',
188             'login': 'Login'
189         }
190 
191         response = self.session.post(self.urls['login'], data=login_params)
192         if not response or not response.text:
193             log.warning('Unable to connect to provider')
194             return False
195 
196         if any([re.search('Username Incorrect', response.text),
197                 re.search('Password Incorrect', response.text), ]):
198             log.warning('Invalid username or password. Check your settings')
199             return False
200 
201         return True
202 
203     def _check_auth(self):
204         """Check if user credentials."""
205         if not self.username or not self.password:
206             raise AuthException('Your authentication credentials for {0} are missing,'
207                                 ' check your config.'.format(self.name))
208 
209         return True
210 
211 
212 provider = NebulanceProvider()
213
```
--- 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/torrent/html/nebulance.py b/medusa/providers/torrent/html/nebulance.py
--- a/medusa/providers/torrent/html/nebulance.py
+++ b/medusa/providers/torrent/html/nebulance.py
@@ -27,6 +27,8 @@
 class NebulanceProvider(TorrentProvider):
     """Nebulance Torrent provider."""
 
+    IDENTIFIER_REGEX = re.compile(r'.+id=([0-9]+)&')
+
     def __init__(self):
         """Initialize the class."""
         super(NebulanceProvider, self).__init__('Nebulance')
@@ -208,5 +210,18 @@
 
         return True
 
+    @staticmethod
+    def _get_identifier(item):
+        """
+        Return the identifier for the item.
+
+        Cut the apikey from it, as this might change over time.
+            So we'd like to prevent adding duplicates to cache.
+        """
+        url = NebulanceProvider.IDENTIFIER_REGEX.match(item.url)
+        if url:
+            return url.group(1)
+        return item.url
+
 
 provider = NebulanceProvider()

{"golden_diff": "diff --git a/medusa/providers/torrent/html/nebulance.py b/medusa/providers/torrent/html/nebulance.py\n--- a/medusa/providers/torrent/html/nebulance.py\n+++ b/medusa/providers/torrent/html/nebulance.py\n@@ -27,6 +27,8 @@\n class NebulanceProvider(TorrentProvider):\n     \"\"\"Nebulance Torrent provider.\"\"\"\n \n+    IDENTIFIER_REGEX = re.compile(r'.+id=([0-9]+)&')\n+\n     def __init__(self):\n         \"\"\"Initialize the class.\"\"\"\n         super(NebulanceProvider, self).__init__('Nebulance')\n@@ -208,5 +210,18 @@\n \n         return True\n \n+    @staticmethod\n+    def _get_identifier(item):\n+        \"\"\"\n+        Return the identifier for the item.\n+\n+        Cut the apikey from it, as this might change over time.\n+            So we'd like to prevent adding duplicates to cache.\n+        \"\"\"\n+        url = NebulanceProvider.IDENTIFIER_REGEX.match(item.url)\n+        if url:\n+            return url.group(1)\n+        return item.url\n+\n \n provider = NebulanceProvider()\n", "issue": "Nebulance - Cached results for manual search get duplicated\n**Describe the bug**\r\nAs described in #8787 \r\nProvider Nebulance duplicates cache results.\r\n![image](https://user-images.githubusercontent.com/4103090/110449268-53085d80-80c2-11eb-9345-5589220a2e42.png)\r\n\r\nIf you need access to the Provider reach out to me in DM on Discord.\r\nI can also provide the cache.db if needed.\r\n\r\nPS: Should rarbg already be deduplicated?\r\n![image](https://user-images.githubusercontent.com/4103090/110449560-9793f900-80c2-11eb-980f-cfd0b0a12252.png)\r\n\n", "before_files": [{"content": "# coding=utf-8\n\n\"\"\"Provider code for Nebulance.\"\"\"\n\nfrom __future__ import unicode_literals\n\nimport logging\nimport re\n\nfrom medusa import tv\nfrom medusa.bs4_parser import BS4Parser\nfrom medusa.helper.common import (\n    convert_size,\n    try_int,\n)\nfrom medusa.helper.exceptions import AuthException\nfrom medusa.logger.adapters.style import BraceAdapter\nfrom medusa.providers.torrent.torrent_provider import TorrentProvider\n\nfrom requests.compat import urljoin\nfrom requests.utils import dict_from_cookiejar\n\nlog = BraceAdapter(logging.getLogger(__name__))\nlog.logger.addHandler(logging.NullHandler())\n\n\nclass NebulanceProvider(TorrentProvider):\n    \"\"\"Nebulance Torrent provider.\"\"\"\n\n    def __init__(self):\n        \"\"\"Initialize the class.\"\"\"\n        super(NebulanceProvider, self).__init__('Nebulance')\n\n        # Credentials\n        self.username = None\n        self.password = None\n\n        # URLs\n        self.url = 'https://nebulance.io/'\n        self.urls = {\n            'login': urljoin(self.url, '/login.php'),\n            'search': urljoin(self.url, '/torrents.php'),\n        }\n\n        # Proper Strings\n\n        # Miscellaneous Options\n        self.freeleech = None\n\n        # Cache\n        self.cache = tv.Cache(self)\n\n    def search(self, search_strings, age=0, ep_obj=None, **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        :param age: Not used\n        :param ep_obj: Not used\n        :returns: A list of search results (structure)\n        \"\"\"\n        results = []\n        if not self.login():\n            return results\n\n        for mode in search_strings:\n            log.debug('Search mode: {0}', mode)\n\n            for search_string in search_strings[mode]:\n\n                if mode != 'RSS':\n                    log.debug('Search string: {search}',\n                              {'search': search_string})\n\n                search_params = {\n                    'searchtext': search_string,\n                    'filter_freeleech': (0, 1)[self.freeleech is True],\n                    'order_by': ('seeders', 'time')[mode == 'RSS'],\n                    'order_way': 'desc',\n                }\n\n                if not search_string:\n                    del search_params['searchtext']\n\n                response = self.session.get(self.urls['search'], 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        items = []\n\n        with BS4Parser(data, 'html5lib') as html:\n            torrent_table = html.find('table', {'id': 'torrent_table'})\n\n            # Continue only if at least one release is found\n            if not torrent_table:\n                log.debug('Data returned from provider does not contain any torrents')\n                return items\n\n            torrent_rows = torrent_table('tr', {'class': 'torrent'})\n\n            # Continue only if one Release is found\n            if not torrent_rows:\n                log.debug('Data returned from provider does not contain any torrents')\n                return items\n\n            for row in torrent_rows:\n                try:\n                    freeleech = row.find('img', alt='Freeleech') is not None\n                    if self.freeleech and not freeleech:\n                        continue\n\n                    download_item = row.find('a', {'title': [\n                        'Download Torrent',  # Download link\n                        'Previously Grabbed Torrent File',  # Already Downloaded\n                        'Currently Seeding Torrent',  # Seeding\n                        'Currently Leeching Torrent',  # Leeching\n                    ]})\n\n                    if not download_item:\n                        continue\n\n                    download_url = urljoin(self.url, download_item['href'])\n\n                    temp_anchor = row.find('a', {'data-src': True})\n                    title = temp_anchor['data-src']\n                    if not all([title, download_url]):\n                        continue\n\n                    cells = row('td')\n                    seeders = try_int(cells[5].text.strip())\n                    leechers = try_int(cells[6].text.strip())\n\n                    # Filter unseeded torrent\n                    if seeders < self.minseed:\n                        if mode != 'RSS':\n                            log.debug(\"Discarding torrent because it doesn't meet the\"\n                                      ' minimum seeders: {0}. Seeders: {1}',\n                                      title, seeders)\n                        continue\n\n                    torrent_size = cells[2].find('div').get_text(strip=True)\n                    size = convert_size(torrent_size) or -1\n\n                    pubdate_raw = cells[3].find('span')['title']\n                    pubdate = self.parse_pubdate(pubdate_raw)\n\n                    item = {\n                        'title': title,\n                        'link': download_url,\n                        'size': size,\n                        'seeders': seeders,\n                        'leechers': leechers,\n                        'pubdate': pubdate,\n                    }\n                    if mode != 'RSS':\n                        log.debug('Found result: {0} with {1} seeders and {2} leechers',\n                                  title, seeders, leechers)\n\n                    items.append(item)\n                except (AttributeError, TypeError, KeyError, ValueError, IndexError):\n                    log.exception('Failed parsing provider.')\n\n        return items\n\n    def login(self):\n        \"\"\"Login method used for logging in before doing search and torrent downloads.\"\"\"\n        if any(dict_from_cookiejar(self.session.cookies).values()):\n            return True\n\n        login_params = {\n            'username': self.username,\n            'password': self.password,\n            'keeplogged': 'on',\n            'login': 'Login'\n        }\n\n        response = self.session.post(self.urls['login'], data=login_params)\n        if not response or not response.text:\n            log.warning('Unable to connect to provider')\n            return False\n\n        if any([re.search('Username Incorrect', response.text),\n                re.search('Password Incorrect', response.text), ]):\n            log.warning('Invalid username or password. Check your settings')\n            return False\n\n        return True\n\n    def _check_auth(self):\n        \"\"\"Check if user credentials.\"\"\"\n        if not self.username or not self.password:\n            raise AuthException('Your authentication credentials for {0} are missing,'\n                                ' check your config.'.format(self.name))\n\n        return True\n\n\nprovider = NebulanceProvider()\n", "path": "medusa/providers/torrent/html/nebulance.py"}], "after_files": [{"content": "# coding=utf-8\n\n\"\"\"Provider code for Nebulance.\"\"\"\n\nfrom __future__ import unicode_literals\n\nimport logging\nimport re\n\nfrom medusa import tv\nfrom medusa.bs4_parser import BS4Parser\nfrom medusa.helper.common import (\n    convert_size,\n    try_int,\n)\nfrom medusa.helper.exceptions import AuthException\nfrom medusa.logger.adapters.style import BraceAdapter\nfrom medusa.providers.torrent.torrent_provider import TorrentProvider\n\nfrom requests.compat import urljoin\nfrom requests.utils import dict_from_cookiejar\n\nlog = BraceAdapter(logging.getLogger(__name__))\nlog.logger.addHandler(logging.NullHandler())\n\n\nclass NebulanceProvider(TorrentProvider):\n    \"\"\"Nebulance Torrent provider.\"\"\"\n\n    IDENTIFIER_REGEX = re.compile(r'.+id=([0-9]+)&')\n\n    def __init__(self):\n        \"\"\"Initialize the class.\"\"\"\n        super(NebulanceProvider, self).__init__('Nebulance')\n\n        # Credentials\n        self.username = None\n        self.password = None\n\n        # URLs\n        self.url = 'https://nebulance.io/'\n        self.urls = {\n            'login': urljoin(self.url, '/login.php'),\n            'search': urljoin(self.url, '/torrents.php'),\n        }\n\n        # Proper Strings\n\n        # Miscellaneous Options\n        self.freeleech = None\n\n        # Cache\n        self.cache = tv.Cache(self)\n\n    def search(self, search_strings, age=0, ep_obj=None, **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        :param age: Not used\n        :param ep_obj: Not used\n        :returns: A list of search results (structure)\n        \"\"\"\n        results = []\n        if not self.login():\n            return results\n\n        for mode in search_strings:\n            log.debug('Search mode: {0}', mode)\n\n            for search_string in search_strings[mode]:\n\n                if mode != 'RSS':\n                    log.debug('Search string: {search}',\n                              {'search': search_string})\n\n                search_params = {\n                    'searchtext': search_string,\n                    'filter_freeleech': (0, 1)[self.freeleech is True],\n                    'order_by': ('seeders', 'time')[mode == 'RSS'],\n                    'order_way': 'desc',\n                }\n\n                if not search_string:\n                    del search_params['searchtext']\n\n                response = self.session.get(self.urls['search'], 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        items = []\n\n        with BS4Parser(data, 'html5lib') as html:\n            torrent_table = html.find('table', {'id': 'torrent_table'})\n\n            # Continue only if at least one release is found\n            if not torrent_table:\n                log.debug('Data returned from provider does not contain any torrents')\n                return items\n\n            torrent_rows = torrent_table('tr', {'class': 'torrent'})\n\n            # Continue only if one Release is found\n            if not torrent_rows:\n                log.debug('Data returned from provider does not contain any torrents')\n                return items\n\n            for row in torrent_rows:\n                try:\n                    freeleech = row.find('img', alt='Freeleech') is not None\n                    if self.freeleech and not freeleech:\n                        continue\n\n                    download_item = row.find('a', {'title': [\n                        'Download Torrent',  # Download link\n                        'Previously Grabbed Torrent File',  # Already Downloaded\n                        'Currently Seeding Torrent',  # Seeding\n                        'Currently Leeching Torrent',  # Leeching\n                    ]})\n\n                    if not download_item:\n                        continue\n\n                    download_url = urljoin(self.url, download_item['href'])\n\n                    temp_anchor = row.find('a', {'data-src': True})\n                    title = temp_anchor['data-src']\n                    if not all([title, download_url]):\n                        continue\n\n                    cells = row('td')\n                    seeders = try_int(cells[5].text.strip())\n                    leechers = try_int(cells[6].text.strip())\n\n                    # Filter unseeded torrent\n                    if seeders < self.minseed:\n                        if mode != 'RSS':\n                            log.debug(\"Discarding torrent because it doesn't meet the\"\n                                      ' minimum seeders: {0}. Seeders: {1}',\n                                      title, seeders)\n                        continue\n\n                    torrent_size = cells[2].find('div').get_text(strip=True)\n                    size = convert_size(torrent_size) or -1\n\n                    pubdate_raw = cells[3].find('span')['title']\n                    pubdate = self.parse_pubdate(pubdate_raw)\n\n                    item = {\n                        'title': title,\n                        'link': download_url,\n                        'size': size,\n                        'seeders': seeders,\n                        'leechers': leechers,\n                        'pubdate': pubdate,\n                    }\n                    if mode != 'RSS':\n                        log.debug('Found result: {0} with {1} seeders and {2} leechers',\n                                  title, seeders, leechers)\n\n                    items.append(item)\n                except (AttributeError, TypeError, KeyError, ValueError, IndexError):\n                    log.exception('Failed parsing provider.')\n\n        return items\n\n    def login(self):\n        \"\"\"Login method used for logging in before doing search and torrent downloads.\"\"\"\n        if any(dict_from_cookiejar(self.session.cookies).values()):\n            return True\n\n        login_params = {\n            'username': self.username,\n            'password': self.password,\n            'keeplogged': 'on',\n            'login': 'Login'\n        }\n\n        response = self.session.post(self.urls['login'], data=login_params)\n        if not response or not response.text:\n            log.warning('Unable to connect to provider')\n            return False\n\n        if any([re.search('Username Incorrect', response.text),\n                re.search('Password Incorrect', response.text), ]):\n            log.warning('Invalid username or password. Check your settings')\n            return False\n\n        return True\n\n    def _check_auth(self):\n        \"\"\"Check if user credentials.\"\"\"\n        if not self.username or not self.password:\n            raise AuthException('Your authentication credentials for {0} are missing,'\n                                ' check your config.'.format(self.name))\n\n        return True\n\n    @staticmethod\n    def _get_identifier(item):\n        \"\"\"\n        Return the identifier for the item.\n\n        Cut the apikey from it, as this might change over time.\n            So we'd like to prevent adding duplicates to cache.\n        \"\"\"\n        url = NebulanceProvider.IDENTIFIER_REGEX.match(item.url)\n        if url:\n            return url.group(1)\n        return item.url\n\n\nprovider = NebulanceProvider()\n", "path": "medusa/providers/torrent/html/nebulance.py"}]}