Repository: Flexget/Flexget

Issue:
BUG: Invalid response received from Sonarr: This series has already been added
<!---
Before opening an issue, verify:

- Is this a feature request? Post it on https://feathub.com/Flexget/Flexget
- Did you recently upgrade? Look at the Change Log and Upgrade Actions to make sure that you don't need to make any changes to your config https://flexget.com/ChangeLog https://flexget.com/UpgradeActions
- Are you running FlexGet as a daemon? Stop it completely and then start it again https://flexget.com/CLI/daemon
- Did you search to see if the issue already exists? https://github.com/Flexget/Flexget/issues
- Did you fill out the issue template as completely as possible?

The issue template is here because it helps to ensure you submitted all the necessary information the first time, and allows us to more quickly review issues. Please fill it out correctly and do not ignore it, no matter how irrelevant you think it may be. Thanks in advance for your help with this!
--->
### Expected behaviour:
<!---
Please don't just say "it doesn't crash" or "it works". Explain what the expected result is.
--->

Skip any shows that are already added on sonarr.

### Actual behaviour:

Doesn't skip the shows that are already added on sonarr so it keeps throwing the error -
`Invalid response received from Sonarr: b'[\r\n {\r\n "propertyName": "TvdbId",\r\n "errorMessage": "This series has already been added",\r\n "attemptedValue": 295829,\r\n "formattedMessageArguments": [],\r\n "formattedMessagePlaceholderValues": {\r\n "propertyName": "Tvdb Id",\r\n "propertyValue": 295829\r\n }\r\n }\r\n]'` whenever the task is executed and done.

The "attemptedValue" and the "propertyValue" are always different, so far it has hit on every show that I already have in sonarr.. It's acting like the shows don't exist at all on sonarr when in reality they do.

### Steps to reproduce:
- Step 1:  Add a show to sonarr
- Step 2: Add same show to a trakt list
- Step 3: Try to import said list into sonarr with the Trakt to Sonarr plugin.

#### Config:

```
variables: secrets.yml

web_server:
  bind: 0.0.0.0
  port: 9091
  web_ui: yes
  
schedules:
##### Import Trakt list "Sonarr-TV" to sonarr
  - tasks: ['import-trakt-to-sonarr']
    interval:
      weeks: 1

tasks:

##### Import Trakt list "Sonarr-TV" to sonarr
  import-trakt-to-sonarr:
    trakt_list:
      username: "{? trakt.account ?}"
      account: "{? trakt.account ?}"
      list: "{? trakt.series ?}"
      type: shows
    accept_all: yes
    list_add:
       - sonarr_list:
          base_url: http://localhost
          port: 8989
          api_key: "{? sonarr.api_key ?}"
          ignore_episodes_with_files: true
          ignore_episodes_without_files: true
          search_missing_episodes: false
```

#### Log:
Running daemon with webUI --
https://pastebin.com/LUV6Jzyq

Running without daemon and not on the webUI  (running through CLI) --
https://pastebin.com/mc3C1FsD

### Additional information:

- FlexGet version: Flexget: 2.11.11  
- Python version: 3.6.3 but have tried with python 2.7 as well
- Installation method: pip
- Using daemon (yes/no): yes but have tried with not running the daemon as well.
- OS and version: Windows 10 Pro x64 bit, 1709 build 16299.64
- Link to crash log:

<!---
In config and debug/crash logs, remember to redact any personal or sensitive information such as passwords, API keys, private URLs and so on.

Please verify that the following data is present before submitting your issue:

- Link to a paste service or paste above the relevant config (preferably full config, including templates if present). Please make sure the paste does not expire, if possible.
- Link to a paste service or paste above debug-level logs of the relevant task/s (use `flexget -L debug execute --tasks <Task_name>`).
- FlexGet version (use `flexget -V` to get it).
- Full Python version, for example `2.7.11` (use `python -V` to get it). Note that FlexGet is not supported for use with Python v3.0, 3.1, 3.2 or 3.6.
- Installation method (pip, git install, etc).
- Whether or not you're running FlexGet as a daemon.
- OS and version.
- Attach crash log if one was generated, in addition to the debug-level log. It can be found in the directory with your config file.
--->

BUG: Invalid response received from Sonarr: This series has already been added
<!---
Before opening an issue, verify:

- Is this a feature request? Post it on https://feathub.com/Flexget/Flexget
- Did you recently upgrade? Look at the Change Log and Upgrade Actions to make sure that you don't need to make any changes to your config https://flexget.com/ChangeLog https://flexget.com/UpgradeActions
- Are you running FlexGet as a daemon? Stop it completely and then start it again https://flexget.com/CLI/daemon
- Did you search to see if the issue already exists? https://github.com/Flexget/Flexget/issues
- Did you fill out the issue template as completely as possible?

The issue template is here because it helps to ensure you submitted all the necessary information the first time, and allows us to more quickly review issues. Please fill it out correctly and do not ignore it, no matter how irrelevant you think it may be. Thanks in advance for your help with this!
--->
### Expected behaviour:
<!---
Please don't just say "it doesn't crash" or "it works". Explain what the expected result is.
--->

Skip any shows that are already added on sonarr.

### Actual behaviour:

Doesn't skip the shows that are already added on sonarr so it keeps throwing the error -
`Invalid response received from Sonarr: b'[\r\n {\r\n "propertyName": "TvdbId",\r\n "errorMessage": "This series has already been added",\r\n "attemptedValue": 295829,\r\n "formattedMessageArguments": [],\r\n "formattedMessagePlaceholderValues": {\r\n "propertyName": "Tvdb Id",\r\n "propertyValue": 295829\r\n }\r\n }\r\n]'` whenever the task is executed and done.

The "attemptedValue" and the "propertyValue" are always different, so far it has hit on every show that I already have in sonarr.. It's acting like the shows don't exist at all on sonarr when in reality they do.

### Steps to reproduce:
- Step 1:  Add a show to sonarr
- Step 2: Add same show to a trakt list
- Step 3: Try to import said list into sonarr with the Trakt to Sonarr plugin.

#### Config:

```
variables: secrets.yml

web_server:
  bind: 0.0.0.0
  port: 9091
  web_ui: yes
  
schedules:
##### Import Trakt list "Sonarr-TV" to sonarr
  - tasks: ['import-trakt-to-sonarr']
    interval:
      weeks: 1

tasks:

##### Import Trakt list "Sonarr-TV" to sonarr
  import-trakt-to-sonarr:
    trakt_list:
      username: "{? trakt.account ?}"
      account: "{? trakt.account ?}"
      list: "{? trakt.series ?}"
      type: shows
    accept_all: yes
    list_add:
       - sonarr_list:
          base_url: http://localhost
          port: 8989
          api_key: "{? sonarr.api_key ?}"
          ignore_episodes_with_files: true
          ignore_episodes_without_files: true
          search_missing_episodes: false
```

#### Log:
Running daemon with webUI --
https://pastebin.com/LUV6Jzyq

Running without daemon and not on the webUI  (running through CLI) --
https://pastebin.com/mc3C1FsD

### Additional information:

- FlexGet version: Flexget: 2.11.11  
- Python version: 3.6.3 but have tried with python 2.7 as well
- Installation method: pip
- Using daemon (yes/no): yes but have tried with not running the daemon as well.
- OS and version: Windows 10 Pro x64 bit, 1709 build 16299.64
- Link to crash log:

<!---
In config and debug/crash logs, remember to redact any personal or sensitive information such as passwords, API keys, private URLs and so on.

Please verify that the following data is present before submitting your issue:

- Link to a paste service or paste above the relevant config (preferably full config, including templates if present). Please make sure the paste does not expire, if possible.
- Link to a paste service or paste above debug-level logs of the relevant task/s (use `flexget -L debug execute --tasks <Task_name>`).
- FlexGet version (use `flexget -V` to get it).
- Full Python version, for example `2.7.11` (use `python -V` to get it). Note that FlexGet is not supported for use with Python v3.0, 3.1, 3.2 or 3.6.
- Installation method (pip, git install, etc).
- Whether or not you're running FlexGet as a daemon.
- OS and version.
- Attach crash log if one was generated, in addition to the debug-level log. It can be found in the directory with your config file.
--->



Discussion:
okay so one thing I did notice today while testing things for #2040 is that it'll still add shows that I don't have on sonarr from my trakt lists, but it still ends up seeing every single show as brand new and not on sonarr (even if they are already) so it continues to try to add everything from the list and continues to throw the same error message.
Well now ran into a bag of mixed results O_o just ran another test against this bug and it didn't throw the error this time with just a random show I had on sonarr and on a trakt list..

Going to investigate and see what shows I have in my list that make the plugin throw this error.
List of shows that end up throwing the error about the series already being added:
https://www.thetvdb.com/?tab=series&id=306304 // A Series of Unfortunate Events
https://www.thetvdb.com/?tab=series&id=295829 // The Man in the High Castle
https://www.thetvdb.com/?tab=series&id=72108 // NCIS
https://www.thetvdb.com/?tab=series&id=152831 // Adventure Time
https://www.thetvdb.com/?tab=series&id=110381 // Archer (2009)
https://www.thetvdb.com/?tab=series&id=255316 // Elementary
https://www.thetvdb.com/?tab=series&id=311816 // The Exorcist
https://www.thetvdb.com/?tab=series&id=280619 // The Expanse
https://www.thetvdb.com/?tab=series&id=296550 // Glitch (2015)
https://www.thetvdb.com/?tab=series&id=311711 // The Good Place
https://www.thetvdb.com/?tab=series&id=271005 // Mountain Monsters
https://www.thetvdb.com/?tab=series&id=270701 // Steven Universe
https://www.thetvdb.com/?tab=series&id=249004 // Neverland
https://www.thetvdb.com/?tab=series&id=77068 // Roseanne
https://www.thetvdb.com/?tab=series&id=336215 // Terror in the Woods
https://www.thetvdb.com/?tab=series&id=310555 // Wrecked (2016)
https://www.thetvdb.com/?tab=series&id=77398 // The X-Files

Hopefully this can help narrow down this issue..

i cant reproduce. There's an explicit check for existence in sonarr for both its supported ids and also via title.
Can you provide full debug logs of this issue please?
For the time until you add your fix I just went and added `only_monitored: false` to my config for my tasks and it seems to have worked for now as I haven't gotten any errors yet.
okay so one thing I did notice today while testing things for #2040 is that it'll still add shows that I don't have on sonarr from my trakt lists, but it still ends up seeing every single show as brand new and not on sonarr (even if they are already) so it continues to try to add everything from the list and continues to throw the same error message.
Well now ran into a bag of mixed results O_o just ran another test against this bug and it didn't throw the error this time with just a random show I had on sonarr and on a trakt list..

Going to investigate and see what shows I have in my list that make the plugin throw this error.
List of shows that end up throwing the error about the series already being added:
https://www.thetvdb.com/?tab=series&id=306304 // A Series of Unfortunate Events
https://www.thetvdb.com/?tab=series&id=295829 // The Man in the High Castle
https://www.thetvdb.com/?tab=series&id=72108 // NCIS
https://www.thetvdb.com/?tab=series&id=152831 // Adventure Time
https://www.thetvdb.com/?tab=series&id=110381 // Archer (2009)
https://www.thetvdb.com/?tab=series&id=255316 // Elementary
https://www.thetvdb.com/?tab=series&id=311816 // The Exorcist
https://www.thetvdb.com/?tab=series&id=280619 // The Expanse
https://www.thetvdb.com/?tab=series&id=296550 // Glitch (2015)
https://www.thetvdb.com/?tab=series&id=311711 // The Good Place
https://www.thetvdb.com/?tab=series&id=271005 // Mountain Monsters
https://www.thetvdb.com/?tab=series&id=270701 // Steven Universe
https://www.thetvdb.com/?tab=series&id=249004 // Neverland
https://www.thetvdb.com/?tab=series&id=77068 // Roseanne
https://www.thetvdb.com/?tab=series&id=336215 // Terror in the Woods
https://www.thetvdb.com/?tab=series&id=310555 // Wrecked (2016)
https://www.thetvdb.com/?tab=series&id=77398 // The X-Files

Hopefully this can help narrow down this issue..

i cant reproduce. There's an explicit check for existence in sonarr for both its supported ids and also via title.
Can you provide full debug logs of this issue please?
For the time until you add your fix I just went and added `only_monitored: false` to my config for my tasks and it seems to have worked for now as I haven't gotten any errors yet.

Resolving patch:
diff --git a/flexget/plugins/list/sonarr_list.py b/flexget/plugins/list/sonarr_list.py
--- a/flexget/plugins/list/sonarr_list.py
+++ b/flexget/plugins/list/sonarr_list.py
@@ -1,27 +1,37 @@
 from __future__ import unicode_literals, division, absolute_import
 from builtins import *  # noqa pylint: disable=unused-import, redefined-builtin
-from future.moves.urllib.parse import urlparse
 
-import json
 import logging
 from collections import MutableSet
 
-import requests
 from requests import RequestException
 
 from flexget import plugin
 from flexget.entry import Entry
 from flexget.event import event
+from flexget.utils import requests
 
 log = logging.getLogger('sonarr_list')
 
+SERIES_ENDPOINT = 'series'
+LOOKUP_ENDPOINT = 'series/lookup'
+PROFILE_ENDPOINT = 'profile'
+ROOTFOLDER_ENDPOINT = 'Rootfolder'
+DELETE_ENDPOINT = 'series/{}'
+
+# Sonarr qualities that do no exist in Flexget
+QUALITY_MAP = {
+    'Raw-HD': 'remux',
+    'DVD': 'dvdrip'
+}
+
 
 class SonarrSet(MutableSet):
     supported_ids = ['tvdb_id', 'tvrage_id', 'tvmaze_id', 'imdb_id', 'slug', 'sonarr_id']
     schema = {
         'type': 'object',
         'properties': {
-            'base_url': {'type': 'string'},
+            'base_url': {'type': 'string', 'default': 'http://localhost'},
             'port': {'type': 'number', 'default': 80},
             'api_key': {'type': 'string'},
             'include_ended': {'type': 'boolean', 'default': True},
@@ -37,116 +47,71 @@ class SonarrSet(MutableSet):
             'series_type': {'type': 'string', 'enum': ['standard', 'daily', 'anime'], 'default': 'standard'},
             'tags': {'type': 'array', 'items': {'type': 'integer'}, 'default': [0]}
         },
-        'required': ['api_key', 'base_url'],
+        'required': ['api_key'],
         'additionalProperties': False
     }
 
-    def series_request_builder(self, base_url, port, api_key):
-        parsedurl = urlparse(base_url)
-        log.debug('Received series list request')
-        url = '%s://%s:%s%s/api/series' % (parsedurl.scheme, parsedurl.netloc, port, parsedurl.path)
-        headers = {'X-Api-Key': api_key}
-        return url, headers
-
-    def lookup_request(self, base_url, port, api_key):
-        parsedurl = urlparse(base_url)
-        log.debug('Received series lookup request')
-        url = '%s://%s:%s%s/api/series/lookup?term=' % (parsedurl.scheme, parsedurl.netloc, port, parsedurl.path)
-        headers = {'X-Api-Key': api_key}
-        return url, headers
-
-    def profile_list_request(self, base_url, port, api_key):
-        parsedurl = urlparse(base_url)
-        log.debug('Received profile list request')
-        url = '%s://%s:%s%s/api/profile' % (parsedurl.scheme, parsedurl.netloc, port, parsedurl.path)
-        headers = {'X-Api-Key': api_key}
-        return url, headers
-
-    def rootfolder_request(self, base_url, port, api_key):
-        parsedurl = urlparse(base_url)
-        log.debug('Received rootfolder list request')
-        url = '%s://%s:%s%s/api/Rootfolder' % (parsedurl.scheme, parsedurl.netloc, port, parsedurl.path)
-        headers = {'X-Api-Key': api_key}
-        return url, headers
-
-    def get_json(self, url, headers):
+    def _sonarr_request(self, endpoint, term=None, method='get', data=None):
+        base_url = self.config['base_url']
+        port = self.config['port']
+        url = '{}:{}/api/{}'.format(base_url, port, endpoint)
+        headers = {'X-Api-Key': self.config['api_key']}
+        if term:
+            url += '?term={}'.format(term)
         try:
-            response = requests.get(url, headers=headers)
-            if response.status_code == 200:
-                return response.json()
-            else:
-                raise plugin.PluginError('Invalid response received from Sonarr: %s' % response.content)
+            rsp = requests.request(method, url, headers=headers, json=data)
+            data = rsp.json()
+            log.trace('sonarr response: %s', data)
+            return data
         except RequestException as e:
-            raise plugin.PluginError('Unable to connect to Sonarr at %s. Error: %s' % (url, e))
-
-    def post_json(self, url, headers, data):
-        try:
-            response = requests.post(url, headers=headers, data=data)
-            if response.status_code == 201:
-                return response.json()
+            base_msg = 'Sonarr returned an error. {}'
+            if e.response is not None:
+                error = e.response.json()[0]
+                error = '{}: {} \'{}\''.format(error['errorMessage'], error['propertyName'], error['attemptedValue'])
             else:
-                raise plugin.PluginError('Invalid response received from Sonarr: %s' % response.content)
-        except RequestException as e:
-            raise plugin.PluginError('Unable to connect to Sonarr at %s. Error: %s' % (url, e))
-
-    def request_builder(self, base_url, request_type, port, api_key):
-        if request_type == 'series':
-            return self.series_request_builder(base_url, port, api_key)
-        elif request_type == 'profile':
-            return self.profile_list_request(base_url, port, api_key)
-        elif request_type == 'lookup':
-            return self.lookup_request(base_url, port, api_key)
-        elif request_type == 'rootfolder':
-            return self.rootfolder_request(base_url, port, api_key)
-        else:
-            raise plugin.PluginError('Received unknown API request, aborting.')
+                error = str(e)
+            raise plugin.PluginError(base_msg.format(error))
 
     def translate_quality(self, quality_name):
         """
-        Translate Sonnar's qualities to ones recognize by Flexget
+        Translate Sonarr's qualities to ones recognize by Flexget
         """
-        if quality_name == 'Raw-HD':  # No better match yet in Flexget
-            return 'remux'
-        elif quality_name == 'DVD':  # No better match yet in Flexget
-            return 'dvdrip'
-        else:
-            return quality_name.replace('-', ' ').lower()
+        if quality_name in QUALITY_MAP:
+            return QUALITY_MAP[quality_name]
+        return quality_name.replace('-', ' ').lower()
 
     def quality_requirement_builder(self, quality_profile):
-
         allowed_qualities = [self.translate_quality(quality['quality']['name']) for quality in quality_profile['items']
                              if quality['allowed']]
         cutoff = self.translate_quality(quality_profile['cutoff']['name'])
 
         return allowed_qualities, cutoff
 
-    def list_entries(self):
-        series_url, series_headers = self.request_builder(self.config.get('base_url'), 'series',
-                                                          self.config.get('port'), self.config['api_key'])
-        json = self.get_json(series_url, series_headers)
-
+    def list_entries(self, filters=True):
+        shows = self._sonarr_request(SERIES_ENDPOINT)
+        profiles_dict = {}
         # Retrieves Sonarr's profile list if include_data is set to true
-        if self.config.get('include_data'):
-            profile_url, profile_headers = self.request_builder(self.config.get('base_url'), 'profile',
-                                                                self.config.get('port'),
-                                                                self.config['api_key'])
-            profiles_json = self.get_json(profile_url, profile_headers)
+        include_data = self.config.get('include_data')
+        if include_data:
+            profiles = self._sonarr_request(PROFILE_ENDPOINT)
+            profiles_dict = {profile['id']: profile for profile in profiles}
 
         entries = []
-        for show in json:
-            fg_qualities = ''  # Initializes the quality parameter
-            fg_cutoff = ''
-            path = None
-            if not show['monitored'] and self.config.get(
-                    'only_monitored'):  # Checks if to retrieve just monitored shows
-                continue
-            if show['status'] == 'ended' and not self.config.get('include_ended'):  # Checks if to retrieve ended shows
-                continue
-            if self.config.get('include_data') and profiles_json:  # Check if to retrieve quality & path
-                path = show.get('path')
-                for profile in profiles_json:
-                    if profile['id'] == show['profileId']:  # Get show's profile data from all possible profiles
-                        fg_qualities, fg_cutoff = self.quality_requirement_builder(profile)
+        for show in shows:
+            fg_qualities = []  # Initializes the quality parameter
+            fg_cutoff = None
+            path = show.get('path') if include_data else None
+            if filters:
+                # Checks if to retrieve just monitored shows
+                if not show['monitored'] and self.config.get('only_monitored'):
+                    continue
+                # Checks if to retrieve ended shows
+                if show['status'] == 'ended' and not self.config.get('include_ended'):
+                    continue
+            profile = profiles_dict.get(show['profileId'])
+            if profile:
+                fg_qualities, fg_cutoff = self.quality_requirement_builder(profile)
+
             entry = Entry(title=show['title'],
                           url='',
                           series_name=show['title'],
@@ -155,17 +120,16 @@ def list_entries(self):
                           tvmaze_id=show.get('tvMazeId'),
                           imdb_id=show.get('imdbid'),
                           slug=show.get('titleSlug'),
-                          sonarr_id=show.get('id'),
-                          configure_series_target=fg_cutoff)
-            if self.config.get('include_data'):
-                if len(fg_qualities) > 1:
-                    entry['configure_series_qualities'] = fg_qualities
-                elif len(fg_qualities) == 1:
-                    entry['configure_series_quality'] = fg_qualities[0]
-                else:
-                    entry['configure_series_quality'] = fg_qualities
-                if path:
-                    entry['configure_series_path'] = path
+                          sonarr_id=show.get('id'))
+            if len(fg_qualities) > 1:
+                entry['configure_series_qualities'] = fg_qualities
+            elif len(fg_qualities) == 1:
+                entry['configure_series_quality'] = fg_qualities[0]
+            if path:
+                entry['configure_series_path'] = path
+            if fg_cutoff:
+                entry['configure_series_target'] = fg_cutoff
+
             if entry.isvalid():
                 log.debug('returning entry %s', entry)
                 entries.append(entry)
@@ -177,26 +141,22 @@ def list_entries(self):
 
     def add_show(self, entry):
         log.debug('searching for show match for %s using Sonarr', entry)
-        lookup_series_url, lookup_series_headers = self.request_builder(self.config.get('base_url'), 'lookup',
-                                                                        self.config.get('port'), self.config['api_key'])
-        if entry.get('tvdb_id'):
-            lookup_series_url += 'tvdb:%s' % entry.get('tvdb_id')
-        else:
-            lookup_series_url += entry.get('title')
-        lookup_results = self.get_json(lookup_series_url, headers=lookup_series_headers)
+        term = 'tvdb:{}'.format(entry['tvdb_id']) if entry.get('tvdb_id') else entry['title']
+        lookup_results = self._sonarr_request(LOOKUP_ENDPOINT, term=term)
         if not lookup_results:
             log.debug('could not find series match to %s', entry)
             return
-        else:
-            if len(lookup_results) > 1:
-                log.debug('got multiple results for Sonarr, using first one')
+        elif len(lookup_results) > 1:
+            log.debug('got multiple results for Sonarr, using first one')
         show = lookup_results[0]
         log.debug('using show %s', show)
-        # Getting rootfolder
-        rootfolder_series_url, rootfolder_series_headers = self.request_builder(self.config.get('base_url'),
-                                                                                'rootfolder', self.config.get('port'),
-                                                                                self.config['api_key'])
-        rootfolder = self.get_json(rootfolder_series_url, headers=rootfolder_series_headers)
+
+        # Getting root folder
+        if self.config.get('root_folder_path'):
+            root_path = self.config['root_folder_path']
+        else:
+            root_folder = self._sonarr_request(ROOTFOLDER_ENDPOINT)
+            root_path = root_folder[0]['path']
 
         # Setting defaults for Sonarr
         show['profileId'] = self.config.get('profile_id')
@@ -205,35 +165,32 @@ def add_show(self, entry):
         show['monitored'] = self.config.get('monitored')
         show['seriesType'] = self.config.get('series_type')
         show['tags'] = self.config.get('tags')
-        show['rootFolderPath'] = self.config.get('root_folder_path', rootfolder[0]['path'])
-        show['addOptions'] = {"ignoreEpisodesWithFiles": self.config.get('ignore_episodes_with_files'),
-                              "ignoreEpisodesWithoutFiles": self.config.get('ignore_episodes_without_files'),
-                              "searchForMissingEpisodes": self.config.get('search_missing_episodes')}
+        show['rootFolderPath'] = root_path
+        show['addOptions'] = {
+            "ignoreEpisodesWithFiles": self.config.get('ignore_episodes_with_files'),
+            "ignoreEpisodesWithoutFiles": self.config.get('ignore_episodes_without_files'),
+            "searchForMissingEpisodes": self.config.get('search_missing_episodes')
+        }
 
-        series_url, series_headers = self.request_builder(self.config.get('base_url'), 'series',
-                                                          self.config.get('port'), self.config['api_key'])
         log.debug('adding show %s to sonarr', show)
-        returned_show = self.post_json(series_url, headers=series_headers, data=json.dumps(show))
+        returned_show = self._sonarr_request(SERIES_ENDPOINT, method='post', data=show)
         return returned_show
 
     def remove_show(self, show):
-        delete_series_url, delete_series_headers = self.request_builder(self.config.get('base_url'), 'series',
-                                                                        self.config.get('port'), self.config['api_key'])
-        delete_series_url += '/%s' % show.get('sonarr_id')
-        requests.delete(delete_series_url, headers=delete_series_headers)
+        log.debug('sending sonarr delete show request')
+        self._sonarr_request(DELETE_ENDPOINT.format(show['sonarr_id']), method='delete')
 
-    @property
-    def shows(self):
+    def shows(self, filters=True):
         if self._shows is None:
-            self._shows = self.list_entries()
+            self._shows = self.list_entries(filters=filters)
         return self._shows
 
-    def _find_entry(self, entry):
-        for sb_entry in self.shows:
-            if any(entry.get(id) is not None and entry[id] == sb_entry[id] for id in self.supported_ids):
-                return sb_entry
-            if entry.get('title').lower() == sb_entry.get('title').lower():
-                return sb_entry
+    def _find_entry(self, entry, filters=True):
+        for show in self.shows(filters=filters):
+            if any(entry.get(id) is not None and entry[id] == show[id] for id in self.supported_ids):
+                return show
+            if entry.get('title').lower() == show.get('title').lower():
+                return show
 
     def _from_iterable(self, it):
         # TODO: is this the right answer? the returned object won't have our custom __contains__ logic
@@ -244,16 +201,16 @@ def __init__(self, config):
         self._shows = None
 
     def __iter__(self):
-        return (entry for entry in self.shows)
+        return (entry for entry in self.shows())
 
     def __len__(self):
-        return len(self.shows)
+        return len(self.shows())
 
     def __contains__(self, entry):
         return self._find_entry(entry) is not None
 
     def add(self, entry):
-        if not self._find_entry(entry):
+        if not self._find_entry(entry, filters=False):
             show = self.add_show(entry)
             if show:
                 self._shows = None
@@ -262,7 +219,7 @@ def add(self, entry):
             log.debug('entry %s already exists in Sonarr list', entry)
 
     def discard(self, entry):
-        show = self._find_entry(entry)
+        show = self._find_entry(entry, filters=False)
         if not show:
             log.debug('Did not find matching show in Sonarr for %s, skipping', entry)
             return


Test patch: