<commit_before>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 = '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', '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},
            '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': 'integer'}, 'default': [0]},
        },
        'required': ['api_key'],
        'additionalProperties': False,
    }

    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 = '{}:{}{}/api/{}'.format(base_url, port, base_path, endpoint)
        headers = {'X-Api-Key': self.config['api_key']}
        if term:
            url += '?term={}'.format(term)
        try:
            rsp = requests.request(method, url, headers=headers, json=data)
            data = rsp.json()
            logger.trace('sonarr response: {}', data)
            return 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))

    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()

    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, 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['profileId'])
            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

    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
        elif len(lookup_results) > 1:
            logger.debug('got multiple results for Sonarr, using first one')
        show = lookup_results[0]
        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['seasonFolder'] = self.config.get('season_folder')
        show['monitored'] = self.config.get('monitored')
        show['seriesType'] = self.config.get('series_type')
        show['tags'] = self.config.get('tags')
        show['rootFolderPath'] = root_path
        show['addOptions'] = {
            "ignoreEpisodesWithFiles": self.config.get('ignore_episodes_with_files'),<commit_msg>[change] Sonarr - Handle when the title or id is not found in the entry object (#2780)<commit_after>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 = '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', '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},
            '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': 'integer'}, 'default': [0]},
        },
        'required': ['api_key'],
        'additionalProperties': False,
    }

    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 = '{}:{}{}/api/{}'.format(base_url, port, base_path, endpoint)
        headers = {'X-Api-Key': self.config['api_key']}
        if term:
            url += '?term={}'.format(term)
        try:
            rsp = requests.request(method, url, headers=headers, json=data)
            data = rsp.json()
            logger.trace('sonarr response: {}', data)
            return 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))

    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()

    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, 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['profileId'])
            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

    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
        elif 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['seasonFolder'] = self.config.get('season_folder')
        show['monitored'] = self.config.get('monitored')
        show['seriesType'] = self.config.get('series_type')
        show['tags'] = self.config.get('tags')
        show['rootFolderPath'] = root_path
        show['addOptions'] = {
            "ignoreEpisodesWithFiles": self.config.get('ignore_episodes_with_files'),
{'fraction_non_alphanumeric': 0.13443730990199393, 'fraction_numerical': 0.0017573504562352145, 'mean_word_length': 2.261904761904762, 'pattern_counts': {'":': 2, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5558784', 'n_tokens_mistral': 4235, 'n_tokens_neox': 4096, 'n_words': 940}