Beispiel #1
0
 def post(self, session: Session = None) -> Response:
     """Perform DB operations"""
     msg = ''
     data = request.json
     operation = data['operation']
     if operation == 'cleanup':
         self.manager.db_cleanup(force=True)
         msg = 'DB Cleanup finished'
     elif operation == 'vacuum':
         session.execute('VACUUM')
         session.commit()
         msg = 'DB VACUUM finished'
     elif operation == 'plugin_reset':
         plugin_name = data.get('plugin_name')
         if not plugin_name:
             raise BadRequest(
                 "'plugin_name' attribute must be used when trying to reset plugin"
             )
         try:
             reset_schema(plugin_name)
             msg = f'Plugin {plugin_name} DB reset was successful'
         except ValueError:
             raise BadRequest(
                 f'The plugin {plugin_name} has no stored schema to reset')
     return success_response(msg)
Beispiel #2
0
    def delete(self, show_id, ep_id, rel_id, session):
        """ Delete episode release by show ID, episode ID and release ID """
        try:
            series.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            series.episode_by_id(ep_id, session)
        except NoResultFound:
            raise NotFoundError('episode with ID %s not found' % ep_id)
        try:
            release = series.release_by_id(rel_id, session)
        except NoResultFound:
            raise NotFoundError('release with ID %s not found' % rel_id)
        if not series.episode_in_show(show_id, ep_id):
            raise BadRequest('episode with id %s does not belong to show %s' %
                             (ep_id, show_id))
        if not series.release_in_episode(ep_id, rel_id):
            raise BadRequest('release id %s does not belong to episode %s' %
                             (rel_id, ep_id))
        args = delete_parser.parse_args()
        if args.get('forget'):
            fire_event('forget', release.title)

        series.delete_release_by_id(rel_id)
        return success_response(
            'successfully deleted release %d from episode %d' %
            (rel_id, ep_id))
Beispiel #3
0
    def delete(self, show_id, season_id, rel_id, session):
        """ Delete episode release by show ID, season ID and release ID """
        try:
            db.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            db.season_by_id(season_id, session)
        except NoResultFound:
            raise NotFoundError('season with ID %s not found' % season_id)
        try:
            release = db.season_release_by_id(rel_id, session)
        except NoResultFound:
            raise NotFoundError('release with ID %s not found' % rel_id)
        if not db.season_in_show(show_id, season_id):
            raise BadRequest('season with id %s does not belong to show %s' %
                             (season_id, show_id))
        if not db.release_in_season(season_id, rel_id):
            raise BadRequest('release id %s does not belong to season %s' %
                             (rel_id, season_id))
        args = delete_parser.parse_args()
        if args.get('forget'):
            fire_event('forget', release.title)

        db.delete_season_release_by_id(rel_id)
        return success_response(
            'successfully deleted release %d from season %d' %
            (rel_id, season_id))
Beispiel #4
0
    def put(self, show_id, ep_id, rel_id, session):
        """ Resets a downloaded release status """
        try:
            series.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            series.episode_by_id(ep_id, session)
        except NoResultFound:
            raise NotFoundError('episode with ID %s not found' % ep_id)
        try:
            release = series.release_by_id(rel_id, session)
        except NoResultFound:
            raise NotFoundError('release with ID %s not found' % rel_id)
        if not series.episode_in_show(show_id, ep_id):
            raise BadRequest('episode with id %s does not belong to show %s' %
                             (ep_id, show_id))
        if not series.release_in_episode(ep_id, rel_id):
            raise BadRequest('release id %s does not belong to episode %s' %
                             (rel_id, ep_id))

        if not release.downloaded:
            raise BadRequest('release with id %s is not set as downloaded' %
                             rel_id)
        release.downloaded = False

        rsp = jsonify(release.to_dict())
        rsp.headers.extend({'Series-ID': show_id, 'Episode-ID': ep_id})
        return rsp
Beispiel #5
0
    def get(self, show_id, ep_id, rel_id, session):
        """ Get episode release by show ID, episode ID and release ID """
        try:
            series.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            series.episode_by_id(ep_id, session)
        except NoResultFound:
            raise NotFoundError('episode with ID %s not found' % ep_id)
        try:
            release = series.release_by_id(rel_id, session)
        except NoResultFound:
            raise NotFoundError('release with ID %s not found' % rel_id)
        if not series.episode_in_show(show_id, ep_id):
            raise BadRequest('episode with id %s does not belong to show %s' % (ep_id, show_id))
        if not series.release_in_episode(ep_id, rel_id):
            raise BadRequest('release id %s does not belong to episode %s' % (rel_id, ep_id))

        rsp = jsonify(release.to_dict())
        rsp.headers.extend({
            'Series-ID': show_id,
            'Episode-ID': ep_id
        })
        return rsp
Beispiel #6
0
    def post(self, session: Session = None) -> Response:
        """ Update config """
        config = {}
        data = request.json
        try:
            raw_config = base64.b64decode(data['raw_config'])
        except (TypeError, binascii.Error):
            raise BadRequest(message='payload was not a valid base64 encoded string')

        try:
            config = yaml.safe_load(raw_config)
        except YAMLError as e:
            if isinstance(e, MarkedYAMLError):
                error: Dict[str, int] = {}
                if e.problem is not None:
                    error.update({'reason': e.problem})
                if e.context_mark is not None:
                    error.update({'line': e.context_mark.line, 'column': e.context_mark.column})
                if e.problem_mark is not None:
                    error.update({'line': e.problem_mark.line, 'column': e.problem_mark.column})
                raise BadRequest(message='Invalid YAML syntax', payload=error)

        try:
            backup_path = self.manager.update_config(config)
        except ConfigError as e:
            errors = []
            for er in e.errors:
                errors.append({'error': er.message, 'config_path': er.json_pointer})
            raise BadRequest(
                message=f'Error loading config: {e.args[0]}', payload={'errors': errors}
            )

        try:
            self.manager.backup_config()
        except Exception as e:
            raise APIError(
                message='Failed to create config backup, config updated but NOT written to file',
                payload={'reason': str(e)},
            )

        try:
            with open(self.manager.config_path, 'w', encoding='utf-8') as f:
                f.write(raw_config.decode('utf-8').replace('\r\n', '\n'))
        except Exception as e:
            raise APIError(
                message='Failed to write new config to file, please load from backup',
                payload={'reason': str(e), 'backup_path': backup_path},
            )
        return success_response('Config was loaded and successfully updated to file')
Beispiel #7
0
    def delete(self, show_id, ep_id, session):
        """ Deletes all episodes releases by show ID and episode ID """
        try:
            series.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            episode = series.episode_by_id(ep_id, session)
        except NoResultFound:
            raise NotFoundError('episode with ID %s not found' % ep_id)
        if not series.episode_in_show(show_id, ep_id):
            raise BadRequest('episode with id %s does not belong to show %s' %
                             (ep_id, show_id))

        args = release_delete_parser.parse_args()
        downloaded = args.get('downloaded') is True if args.get(
            'downloaded') is not None else None
        release_items = []
        for release in episode.releases:
            if downloaded and release.downloaded or downloaded is False and not release.downloaded or not downloaded:
                release_items.append(release)

        for release in release_items:
            if args.get('forget'):
                fire_event('forget', release.title)
            series.delete_release_by_id(release.id)
        return success_response(
            'successfully deleted all releases for episode %s from show %s' %
            (ep_id, show_id))
Beispiel #8
0
    def get(self, session=None):
        """TheTVDB series search"""
        args = search_parser.parse_args()
        language = args['language']

        search_name = args.get('search_name')
        imdb_id = args.get('imdb_id')
        zap2it_id = args.get('zap2it_id')
        force_search = args.get('force_search')

        if not any(arg for arg in [search_name, imdb_id, zap2it_id]):
            raise BadRequest('Not enough lookup arguments')
        kwargs = {
            'search_name': search_name,
            'imdb_id': imdb_id,
            'zap2it_id': zap2it_id,
            'force_search': force_search,
            'session': session,
            'language': language,
        }
        try:
            search_results = search_for_series(**kwargs)
        except LookupError as e:
            raise NotFoundError(e.args[0])
        return jsonify([a.to_dict() for a in search_results])
Beispiel #9
0
    def delete(self, show_id, season_id, session):
        """ Deletes all season releases by show ID and season ID """
        try:
            db.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            season = db.season_by_id(season_id, session)
        except NoResultFound:
            raise NotFoundError('seasons with ID %s not found' % season_id)
        if not db.season_in_show(show_id, season_id):
            raise BadRequest('season with id %s does not belong to show %s' %
                             (season_id, show_id))

        args = release_delete_parser.parse_args()
        downloaded = args.get('downloaded') is True if args.get(
            'downloaded') is not None else None
        release_items = []
        for release in season.releases:
            if (downloaded and release.downloaded
                    or downloaded is False and not release.downloaded
                    or not downloaded):
                release_items.append(release)

        for release in release_items:
            if args.get('forget'):
                fire_event('forget', release.title)
            db.delete_season_release_by_id(release.id)
        return success_response(
            'successfully deleted all releases for season %s from show %s' %
            (season_id, show_id))
Beispiel #10
0
 def get(self, session=None):
     """Get channel status enumeration meaning"""
     try:
         from irc_bot import simple_irc_bot
     except ImportError:
         raise BadRequest('irc_bot dep is not installed')
     return jsonify(simple_irc_bot.IRCChannelStatus().enum_dict)
Beispiel #11
0
 def post(self, list_id, session=None):
     """ Add movies to list by ID """
     try:
         ml.get_list_by_id(list_id=list_id, session=session)
     except NoResultFound:
         raise NotFoundError('list_id %d does not exist' % list_id)
     data = request.json
     movie_identifiers = data.get('movie_identifiers', [])
     # Validates ID type based on allowed ID
     for id_name in movie_identifiers:
         if list(id_name)[0] not in MovieListBase().supported_ids:
             raise BadRequest('movie identifier %s is not allowed' %
                              id_name)
     title, year = data['movie_name'], data.get('movie_year')
     movie = ml.get_movie_by_title_and_year(list_id=list_id,
                                            title=title,
                                            year=year,
                                            session=session)
     if movie:
         raise Conflict('movie with name "%s" already exist in list %d' %
                        (title, list_id))
     movie = ml.MovieListMovie()
     movie.title = title
     movie.year = year
     movie.ids = ml.get_db_movie_identifiers(
         identifier_list=movie_identifiers, session=session)
     movie.list_id = list_id
     session.add(movie)
     session.commit()
     response = jsonify(movie.to_dict())
     response.status_code = 201
     return response
Beispiel #12
0
    def get(self, session=None):
        """ Get TMDB movie data """
        args = tmdb_parser.parse_args()
        title = args.get('title')
        tmdb_id = args.get('tmdb_id')
        imdb_id = args.get('imdb_id')

        posters = args.pop('include_posters', False)
        backdrops = args.pop('include_backdrops', False)

        if not (title or tmdb_id or imdb_id):
            raise BadRequest(description)

        lookup = plugin.get('api_tmdb', 'tmdb.api').lookup

        try:
            movie = lookup(session=session, **args)
        except LookupError as e:
            raise NotFoundError(e.args[0])

        return_movie = movie.to_dict()

        if posters:
            return_movie['posters'] = [p.to_dict() for p in movie.posters]

        if backdrops:
            return_movie['backdrops'] = [p.to_dict() for p in movie.backdrops]

        return jsonify(return_movie)
Beispiel #13
0
    def get(self, tvdb_id, session=None):
        args = episode_parser.parse_args()
        language = args['language']

        absolute_number = args.get('absolute_number')
        season_number = args.get('season_number')
        ep_number = args.get('ep_number')
        air_date = args.get('air_date')

        if not ((season_number and ep_number) or absolute_number or air_date):
            raise BadRequest('not enough parameters for lookup. Either season and episode number or absolute number '
                             'are required.')
        kwargs = {'tvdb_id': tvdb_id,
                  'session': session,
                  'language': language}

        if absolute_number:
            kwargs['absolute_number'] = absolute_number
        if season_number and ep_number:
            kwargs['season_number'] = season_number
            kwargs['episode_number'] = ep_number
        if air_date:
            kwargs['first_aired'] = air_date

        try:
            episode = lookup_episode(**kwargs)
        except LookupError as e:
            raise NotFoundError(e.args[0])
        return jsonify(episode.to_dict())
Beispiel #14
0
    def get(self, session=None):
        """ List of previously accepted entries """
        args = history_parser.parse_args()

        # Pagination and sorting params
        page = args['page']
        per_page = args['per_page']
        sort_by = args['sort_by']
        sort_order = args['order']

        # Hard limit results per page to 100
        if per_page > 100:
            per_page = 100

        # Filter param
        task = args['task']

        # Build query
        query = session.query(db.History)
        if task:
            query = query.filter(db.History.task == task)

        total_items = query.count()

        if not total_items:
            return jsonify([])

        total_pages = int(ceil(total_items / float(per_page)))

        if page > total_pages:
            raise NotFoundError('page %s does not exist' % page)

        start = (page - 1) * per_page
        finish = start + per_page

        # Choose sorting order
        order = desc if sort_order == 'desc' else asc

        # Get items
        try:
            items = query.order_by(order(getattr(db.History, sort_by))).slice(
                start, finish)
        except AttributeError as e:
            raise BadRequest(str(e))

        # Actual results in page
        actual_size = min(items.count(), per_page)

        # Get pagination headers
        pagination = pagination_headers(total_pages, total_items, actual_size,
                                        request)

        # Create response
        rsp = jsonify([item.to_dict() for item in items])

        # Add link header to response
        rsp.headers.extend(pagination)
        return rsp
Beispiel #15
0
 def get(self, session=None):
     """ Reset the DB of a specific plugin """
     args = plugin_parser.parse_args()
     plugin = args['plugin_name']
     try:
         reset_schema(plugin)
     except ValueError:
         raise BadRequest('The plugin {} has no stored schema to reset'.format(plugin))
     return success_response('Plugin {} DB reset was successful'.format(plugin))
Beispiel #16
0
 def put(self, session=None):
     """ Change user password """
     user = current_user
     data = request.json
     try:
         change_password(username=user.name, password=data.get('password'), session=session)
     except WeakPassword as e:
         raise BadRequest(e.value)
     return success_response('Successfully changed user password')
Beispiel #17
0
 def get(self, plugin_name, session=None):
     """ Return plugin data by name"""
     args = plugin_parser.parse_args()
     try:
         plugin = get_plugin_by_name(plugin_name, issued_by='plugins API')
     except DependencyError as e:
         raise BadRequest(e.message)
     p = plugin_to_dict(plugin)
     if args['include_schema']:
         p['schema'] = plugin.schema
     return jsonify(p)
Beispiel #18
0
 def get(self, session=None):
     """ Cache remote resources """
     args = cached_parser.parse_args()
     url = args.get('url')
     force = args.get('force')
     try:
         file_path, mime_type = cached_resource(url, self.manager.config_base, force=force)
     except RequestException as e:
         raise BadRequest('Request Error: {}'.format(e.args[0]))
     except OSError as e:
         raise APIError('Error: {}'.format(str(e)))
     return send_file(file_path, mimetype=mime_type)
Beispiel #19
0
    def get(self, session=None):
        """Returns status of IRC connections"""
        from flexget.plugins.daemon.irc import irc_manager
        if irc_manager is None:
            raise BadRequest('IRC daemon does not appear to be running')

        args = irc_parser.parse_args()
        name = args.get('name')
        try:
            status = irc_manager.status(name)
        except ValueError as e:
            raise NotFoundError(e.args[0])
        return jsonify(status)
Beispiel #20
0
    def get(self, session=None):
        """Restarts IRC connections"""
        from .irc import irc_manager

        if irc_manager is None:
            raise BadRequest('IRC daemon does not appear to be running')

        args = irc_parser.parse_args()
        connection = args.get('name')
        try:
            irc_manager.restart_connections(connection)
        except KeyError:
            raise NotFoundError('Connection {} is not a valid IRC connection'.format(connection))
        return success_response('Successfully restarted connection(s)')
Beispiel #21
0
    def get(self, show_id, season_id, rel_id, session):
        """ Get season release by show ID, season ID and release ID """
        try:
            db.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            db.season_by_id(season_id, session)
        except NoResultFound:
            raise NotFoundError('season with ID %s not found' % season_id)
        try:
            release = db.season_release_by_id(rel_id, session)
        except NoResultFound:
            raise NotFoundError('release with ID %s not found' % rel_id)
        if not db.season_in_show(show_id, season_id):
            raise BadRequest('season with id %s does not belong to show %s' %
                             (season_id, show_id))
        if not db.release_in_season(season_id, rel_id):
            raise BadRequest('release id %s does not belong to season %s' %
                             (rel_id, season_id))

        rsp = jsonify(release.to_dict())
        rsp.headers.extend({'Series-ID': show_id, 'Season-ID': season_id})
        return rsp
Beispiel #22
0
    def get(self, session=None):
        """Stops IRC connections"""
        from flexget.plugins.daemon.irc import irc_manager
        if irc_manager is None:
            raise BadRequest('IRC daemon does not appear to be running')

        args = irc_stop_parser.parse_args()
        name = args.get('name')
        wait = args.get('wait')
        try:
            irc_manager.stop_connections(wait=wait, name=name)
        except KeyError:
            raise NotFoundError(
                'Connection {} is not a valid IRC connection'.format(name))
        return success_response('Successfully stopped connection(s)')
Beispiel #23
0
    def put(self, task, session: Session = None) -> Response:
        """ Update tasks config """
        data = request.json

        new_task_name = data['name']

        if task not in self.manager.user_config.get('tasks', {}):
            raise NotFoundError(f'task `{task}` not found')

        if 'tasks' not in self.manager.user_config:
            self.manager.user_config['tasks'] = {}
        if 'tasks' not in self.manager.config:
            self.manager.config['tasks'] = {}

        if task != new_task_name:
            # Rename task
            if new_task_name in self.manager.user_config['tasks']:
                raise BadRequest('cannot rename task as it already exist')

            del self.manager.user_config['tasks'][task]
            del self.manager.config['tasks'][task]

        # Process the task config
        task_schema_processed = copy.deepcopy(data)
        errors = process_config(task_schema_processed,
                                schema=task_return_schema.__schema__,
                                set_defaults=True)

        if errors:
            raise APIError(
                'problem loading config, raise a BUG as this should not happen!'
            )

        self.manager.user_config['tasks'][new_task_name] = data['config']
        self.manager.config['tasks'][new_task_name] = task_schema_processed[
            'config']

        self.manager.save_config()
        self.manager.config_changed()

        rsp = jsonify({
            'name':
            new_task_name,
            'config':
            self.manager.user_config['tasks'][new_task_name]
        })
        rsp.status_code = 200
        return rsp
Beispiel #24
0
    def get(self, session=None):
        """ Get list of registered plugins """
        args = plugins_parser.parse_args()

        # Pagination and sorting params
        page = args['page']
        per_page = args['per_page']

        # Handle max size limit
        if per_page > 100:
            per_page = 100

        start = per_page * (page - 1)
        stop = start + per_page

        plugin_list = []
        try:
            for plugin in get_plugins(phase=args['phase'],
                                      interface=args['interface']):
                p = plugin_to_dict(plugin)
                if args['include_schema']:
                    p['schema'] = plugin.schema
                plugin_list.append(p)
        except ValueError as e:
            raise BadRequest(str(e))

        total_items = len(plugin_list)

        sliced_list = plugin_list[start:stop]

        # Total number of pages
        total_pages = int(ceil(total_items / float(per_page)))

        if page > total_pages and total_pages != 0:
            raise NotFoundError('page %s does not exist' % page)

        # Actual results in page
        actual_size = min(per_page, len(sliced_list))

        # Get pagination headers
        pagination = pagination_headers(total_pages, total_items, actual_size,
                                        request)

        rsp = jsonify(sliced_list)

        # Add link header to response
        rsp.headers.extend(pagination)
        return rsp
Beispiel #25
0
    def delete(self, show_id, ep_id, session):
        """ Forgets episode by show ID and episode ID """
        try:
            show = series.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            episode = series.episode_by_id(ep_id, session)
        except NoResultFound:
            raise NotFoundError('episode with ID %s not found' % ep_id)
        if not series.episode_in_show(show_id, ep_id):
            raise BadRequest('episode with id %s does not belong to show %s' % (ep_id, show_id))

        args = delete_parser.parse_args()
        series.remove_series_entity(show.name, episode.identifier, args.get('forget'))

        return success_response('successfully removed episode %s from show %s' % (ep_id, show_id))
Beispiel #26
0
    def put(self, entry_id, session=None):
        """Approve/Reject the status of a pending entry"""
        try:
            entry = db.get_entry_by_id(session, entry_id)
        except NoResultFound:
            raise NotFoundError('No pending entry with ID %s' % entry_id)

        data = request.json
        approved = data['operation'] == 'approve'
        operation_text = 'approved' if approved else 'pending'
        if entry.approved is approved:
            raise BadRequest('Entry with id {} is already {}'.format(entry_id, operation_text))

        entry.approved = approved
        session.commit()
        rsp = jsonify(entry.to_dict())
        rsp.status_code = 201
        return rsp
Beispiel #27
0
    def get(self, show_id, ep_id, session):
        """ Get episode by show ID and episode ID"""
        try:
            series.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            episode = series.episode_by_id(ep_id, session)
        except NoResultFound:
            raise NotFoundError('episode with ID %s not found' % ep_id)
        if not series.episode_in_show(show_id, ep_id):
            raise BadRequest('episode with id %s does not belong to show %s' % (ep_id, show_id))

        rsp = jsonify(episode.to_dict())

        # Add Series-ID header
        rsp.headers.extend({'Series-ID': show_id})
        return rsp
Beispiel #28
0
    def get(self, show_id, season_id, session):
        """ Get season by show ID and season ID"""
        try:
            db.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            season = db.season_by_id(season_id, session)
        except NoResultFound:
            raise NotFoundError('season with ID %s not found' % season_id)
        if not db.season_in_show(show_id, season_id):
            raise BadRequest('season with id %s does not belong to show %s' %
                             (season_id, show_id))

        rsp = jsonify(season.to_dict())

        # Add Series-ID header
        rsp.headers.extend({'Series-ID': show_id})
        return rsp
Beispiel #29
0
    def put(self, show_id, ep_id, session):
        """ Marks all downloaded releases as not downloaded """
        try:
            series.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            episode = series.episode_by_id(ep_id, session)
        except NoResultFound:
            raise NotFoundError('episode with ID %s not found' % ep_id)
        if not series.episode_in_show(show_id, ep_id):
            raise BadRequest('episode with id %s does not belong to show %s' % (ep_id, show_id))

        for release in episode.releases:
            if release.downloaded:
                release.downloaded = False

        return success_response(
            'successfully reset download status for all releases for episode %s from show %s' % (ep_id, show_id))
Beispiel #30
0
    def delete(self, show_id, season_id, session):
        """ Forgets season by show ID and season ID """
        try:
            show = db.show_by_id(show_id, session=session)
        except NoResultFound:
            raise NotFoundError('show with ID %s not found' % show_id)
        try:
            season = db.season_by_id(season_id, session)
        except NoResultFound:
            raise NotFoundError('season with ID %s not found' % season_id)
        if not db.season_in_show(show_id, season_id):
            raise BadRequest('season with id %s does not belong to show %s' %
                             (season_id, show_id))

        args = delete_parser.parse_args()
        db.remove_series_entity(show.name, season.identifier,
                                args.get('forget'))

        return success_response('successfully removed season %s from show %s' %
                                (season_id, show_id))