Source code for flexget.components.managed_lists.lists.sonarr_list

from collections.abc import MutableSet

from loguru import logger
from requests import RequestException

from flexget import plugin
from flexget.entry import Entry
from flexget.event import event
from flexget.utils import requests

logger = logger.bind(name='sonarr_list')

SERIES_ENDPOINT = 'series'
LOOKUP_ENDPOINT = 'series/lookup'
PROFILE_ENDPOINT = 'qualityProfile'
ROOTFOLDER_ENDPOINT = 'Rootfolder'
DELETE_ENDPOINT = 'series/{}'

# Sonarr qualities that do no exist in Flexget
QUALITY_MAP = {'Raw-HD': 'remux', 'DVD': 'dvdrip'}


[docs] class SonarrSet(MutableSet): supported_ids = ['tvdb_id', 'tvrage_id', 'tvmaze_id', 'imdb_id', 'slug', 'sonarr_id'] schema = { 'type': 'object', 'properties': { 'base_url': {'type': 'string', 'default': 'http://localhost'}, 'base_path': {'type': 'string', 'default': ''}, 'port': {'type': 'number', 'default': 80}, 'api_key': {'type': 'string'}, 'include_ended': {'type': 'boolean', 'default': True}, 'only_monitored': {'type': 'boolean', 'default': True}, 'include_data': {'type': 'boolean', 'default': False}, 'search_missing_episodes': {'type': 'boolean', 'default': True}, 'ignore_episodes_without_files': {'type': 'boolean', 'default': False}, 'ignore_episodes_with_files': {'type': 'boolean', 'default': False}, 'profile_id': {'type': 'integer', 'default': 1}, 'language_id': {'type': 'integer', 'default': 1}, 'season_folder': {'type': 'boolean', 'default': False}, 'monitored': {'type': 'boolean', 'default': True}, 'root_folder_path': {'type': 'string'}, 'series_type': { 'type': 'string', 'enum': ['standard', 'daily', 'anime'], 'default': 'standard', }, 'tags': {'type': 'array', 'items': {'type': 'string'}}, }, 'required': ['api_key'], 'additionalProperties': False, } def __init__(self, config): self.config = config self._shows = None # cache tags self._tags = None
[docs] def _sonarr_request(self, endpoint, term=None, method='get', data=None): base_url = self.config['base_url'] port = self.config['port'] base_path = self.config['base_path'] url = f'{base_url}:{port}{base_path}/api/v3/{endpoint}' headers = {'X-Api-Key': self.config['api_key']} if term: url += f'?term={term}' try: rsp = requests.request(method, url, headers=headers, json=data) data = rsp.json() logger.trace('sonarr response: {}', data) except RequestException as e: 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: error = str(e) raise plugin.PluginError(base_msg.format(error)) return data
[docs] def translate_quality(self, quality_name): """Translate Sonarr's qualities to ones recognize by Flexget.""" if quality_name in QUALITY_MAP: return QUALITY_MAP[quality_name] return quality_name.replace('-', ' ').lower()
[docs] 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
[docs] def get_tag_ids(self, entry): tags_ids = [] if not self._tags: self._tags = {t['label'].lower(): t['id'] for t in self._sonarr_request('tag')} for tag in self.config.get('tags', []): if isinstance(tag, int): # Handle tags by id if tag not in self._tags.values(): logger.error( 'Unable to add tag with id {} to entry {} as the tag does not exist in sonarr', entry, tag, ) continue tags_ids.append(tag) else: tag = entry.render(tag).lower() found = self._tags.get(tag) if not found: logger.verbose('Adding missing tag {} to Sonarr', tag) found = self._sonarr_request('tag', method='post', data={'label': tag})['id'] self._tags[tag] = found tags_ids.append(found) return tags_ids
[docs] 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 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 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['qualityProfileId']) if profile: fg_qualities, fg_cutoff = self.quality_requirement_builder(profile) entry = Entry( title=show['title'], url='', series_name=show['title'], tvdb_id=show.get('tvdbId'), tvrage_id=show.get('tvRageId'), tvmaze_id=show.get('tvMazeId'), imdb_id=show.get('imdbid'), slug=show.get('titleSlug'), 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(): logger.debug('returning entry {}', entry) entries.append(entry) else: logger.error('Invalid entry created? {}', entry) continue return entries
[docs] def add_show(self, entry): logger.debug('searching for show match for {} using Sonarr', entry) 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: logger.debug('could not find series match to {}', entry) return None if len(lookup_results) > 1: logger.debug('got multiple results for Sonarr, using first one') show = lookup_results[0] if show.get('id'): logger.debug('entry {} already exists in Sonarr list as show {}', entry, show) return show logger.debug('using show {}', show) # 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') show['qualityProfileId'] = self.config.get('profile_id') show['languageProfileId'] = self.config.get('language_id') show['seasonFolder'] = self.config.get('season_folder') show['monitored'] = self.config.get('monitored') show['seriesType'] = self.config.get('series_type') show['tags'] = self.get_tag_ids(entry) 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'), } logger.debug('adding show {} to sonarr', show) return self._sonarr_request(SERIES_ENDPOINT, method='post', data=show)
[docs] def remove_show(self, show): logger.debug('sending sonarr delete show request') self._sonarr_request(DELETE_ENDPOINT.format(show['sonarr_id']), method='delete')
[docs] def shows(self, filters=True): if self._shows is None: self._shows = self.list_entries(filters=filters) return self._shows
[docs] 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 return None
[docs] def _from_iterable(self, it): # TODO: is this the right answer? the returned object won't have our custom __contains__ logic return set(it)
def __iter__(self): return (entry for entry in self.shows()) def __len__(self): return len(self.shows()) def __contains__(self, entry): return self._find_entry(entry) is not None
[docs] def add(self, entry): if not self._find_entry(entry, filters=False): show = self.add_show(entry) if show: self._shows = None logger.verbose('Successfully added show {} to Sonarr', show['title']) else: logger.debug('entry {} already exists in Sonarr list', entry)
[docs] def discard(self, entry): show = self._find_entry(entry, filters=False) if not show: logger.debug('Did not find matching show in Sonarr for {}, skipping', entry) return self.remove_show(show) logger.verbose('removed show {} from Sonarr', show['title'])
@property def immutable(self): return False @property def online(self): """Set the online status of the plugin. Online plugin should be treated differently in certain situations, like test mode """ return True
[docs] def get(self, entry): return self._find_entry(entry)
[docs] class SonarrList: schema = SonarrSet.schema
[docs] @staticmethod def get_list(config): return SonarrSet(config)
[docs] def on_task_input(self, task, config): return list(SonarrSet(config))
[docs] @event('plugin.register') def register_plugin(): plugin.register(SonarrList, 'sonarr_list', api_ver=2, interfaces=['task', 'list'])