def delete(self, series_slug, episode_slug, **kwargs): """Delete the episode.""" if not series_slug: return self._method_not_allowed( 'Deleting multiple series are not allowed') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: return self._bad_request('Invalid episode number') episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: return self._not_found('Episode not found') try: episode.delete_episode() except EpisodeDeletedException: return self._no_content() else: return self._conflict('Unable to delete episode')
def http_post(self, identifier, **kwargs): """Add an alias.""" if identifier is not None: return self._bad_request('Alias id should not be specified') data = json_decode(self.request.body) if not data or not all([data.get('series'), data.get('name'), data.get('type')]) or 'id' in data or data['type'] != 'local': return self._bad_request('Invalid request body') series_identifier = SeriesIdentifier.from_slug(data.get('series')) if not series_identifier: return self._bad_request('Invalid series') cache_db_con = db.DBConnection('cache.db') last_changes = cache_db_con.connection.total_changes cursor = cache_db_con.action('INSERT INTO scene_exceptions' ' (indexer, indexer_id, show_name, season, custom) ' ' values (?,?,?,?,1)', [series_identifier.indexer.id, series_identifier.id, data['name'], data.get('season')]) if cache_db_con.connection.total_changes - last_changes <= 0: return self._conflict('Unable to create alias') data['id'] = cursor.lastrowid return self._created(data=data, identifier=data['id'])
def get_show_from_slug(slug): identifier = SeriesIdentifier.from_slug(slug) if not identifier: raise ChangeIndexerException(f'Could not create identifier with slug {slug}') show = Series.find_by_identifier(identifier) return show
def patch(self, series_slug, episode_slug=None, path_param=None): """Patch episode.""" series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') data = json_decode(self.request.body) # Multi-patch request if not episode_slug: return self._patch_multi(series, data) episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: return self._bad_request('Invalid episode number') episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: return self._not_found('Episode not found') accepted = self._patch_episode(episode, data) return self._ok(data=accepted)
def http_post(self, series_slug=None, path_param=None): """Add a new series.""" if series_slug is not None: return self._bad_request('Series slug should not be specified') data = json_decode(self.request.body) if not data or 'id' not in data: return self._bad_request('Invalid series data') ids = {k: v for k, v in viewitems(data['id']) if k != 'imdb'} if len(ids) != 1: return self._bad_request('Only 1 indexer identifier should be specified') identifier = SeriesIdentifier.from_slug('{slug}{id}'.format(slug=list(ids)[0], id=list(itervalues(ids))[0])) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if series: return self._conflict('Series already exist added') series = Series.from_identifier(identifier) if not Series.save_series(series): return self._not_found('Series not found in the specified indexer') return self._created(series.to_json(), identifier=identifier.slug)
def get_results(self, show_slug=None, season=None, episode=None): """Get cached results for this provider.""" cache_db_con = self._get_db() param = [] where = [] if show_slug: show = SeriesIdentifier.from_slug(show_slug) where += ['indexer', 'indexerid'] param += [show.indexer.id, show.id] if season: where += ['season'] param += [season] if episode: where += ['episodes'] param += ['|{0}|'.format(episode)] base_sql = 'SELECT * FROM [{name}]'.format(name=self.provider_id) base_params = [] if where and param: base_sql += ' WHERE ' base_sql += ' AND '.join([item + ' = ? ' for item in where]) base_params += param results = cache_db_con.select( base_sql, base_params ) return results
def post(self, series_slug=None, path_param=None): """Add a new series.""" if series_slug is not None: return self._bad_request('Series slug should not be specified') data = json_decode(self.request.body) if not data or 'id' not in data: return self._bad_request('Invalid series data') ids = {k: v for k, v in viewitems(data['id']) if k != 'imdb'} if len(ids) != 1: return self._bad_request( 'Only 1 indexer identifier should be specified') identifier = SeriesIdentifier.from_slug('{slug}{id}'.format( slug=list(ids)[0], id=list(itervalues(ids))[0])) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if series: return self._conflict('Series already exist added') series = Series.from_identifier(identifier) if not Series.save_series(series): return self._not_found('Series not found in the specified indexer') return self._created(series.to_json(), identifier=identifier.slug)
def post(self, identifier, **kwargs): """Add an alias.""" if identifier is not None: return self._bad_request('Alias id should not be specified') data = json_decode(self.request.body) if not data or not all( [data.get('series'), data.get('name'), data.get('type')]) or 'id' in data or data['type'] != 'local': return self._bad_request('Invalid request body') series_identifier = SeriesIdentifier.from_slug(data.get('series')) if not series_identifier: return self._bad_request('Invalid series') cache_db_con = db.DBConnection('cache.db') last_changes = cache_db_con.connection.total_changes cursor = cache_db_con.action( b'INSERT INTO scene_exceptions' b' (indexer, indexer_id, show_name, season, custom) ' b' values (?,?,?,?,1)', [ series_identifier.indexer.id, series_identifier.id, data['name'], data.get('season') ]) if cache_db_con.connection.total_changes - last_changes <= 0: return self._conflict('Unable to create alias') data['id'] = cursor.lastrowid return self._created(data=data, identifier=data['id'])
def get(self, series_slug, identifier): """Query series information. :param series_slug: series slug. E.g.: tvdb1234 :param identifier: """ series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') if identifier == 'backlogged': # TODO: revisit allowed_qualities = self._parse( self.get_argument('allowed', default=None), str) allowed_qualities = list(map( int, allowed_qualities.split(','))) if allowed_qualities else [] preferred_qualities = self._parse( self.get_argument('preferred', default=None), str) preferred_qualities = list(map(int, preferred_qualities.split( ','))) if preferred_qualities else [] new, existing = series.get_backlogged_episodes( allowed_qualities=allowed_qualities, preferred_qualities=preferred_qualities) data = {'new': new, 'existing': existing} return self._ok(data=data) return self._bad_request('Invalid request')
def http_put(self, identifier, **kwargs): """Update alias information.""" identifier = self._parse(identifier) if not identifier: return self._not_found('Invalid alias id') data = json_decode(self.request.body) if not data or not all([data.get('id'), data.get('series'), data.get('name'), data.get('type')]) or data['id'] != identifier: return self._bad_request('Invalid request body') series_identifier = SeriesIdentifier.from_slug(data.get('series')) if not series_identifier: return self._bad_request('Invalid series') cache_db_con = db.DBConnection('cache.db') last_changes = cache_db_con.connection.total_changes cache_db_con.action('UPDATE scene_exceptions' ' set indexer = ?' ', indexer_id = ?' ', show_name = ?' ', season = ?' ', custom = 1' ' WHERE exception_id = ?', [series_identifier.indexer.id, series_identifier.id, data['name'], data.get('season'), identifier]) if cache_db_con.connection.total_changes - last_changes != 1: return self._not_found('Alias not found') return self._no_content()
def http_delete(self, series_slug, episode_slug, **kwargs): """Delete the episode.""" if not series_slug: return self._method_not_allowed('Deleting multiple series are not allowed') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: return self._bad_request('Invalid episode number') episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: return self._not_found('Episode not found') try: episode.delete_episode() except EpisodeDeletedException: return self._no_content() else: return self._conflict('Unable to delete episode')
def http_patch(self, series_slug, episode_slug=None, path_param=None): """Patch episode.""" series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') data = json_decode(self.request.body) # Multi-patch request if not episode_slug: return self._patch_multi(series, data) episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: return self._bad_request('Invalid episode number') episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: return self._not_found('Episode not found') accepted = self._patch_episode(episode, data) return self._ok(data=accepted)
def get(self, series_slug, path_param=None): """Query series information. :param series_slug: series slug. E.g.: tvdb1234 :param path_param: """ arg_paused = self._parse_boolean(self.get_argument('paused', default=None)) def filter_series(current): return arg_paused is None or current.paused == arg_paused if not series_slug: detailed = self._parse_boolean(self.get_argument('detailed', default=False)) data = [s.to_json(detailed=detailed) for s in Series.find_series(predicate=filter_series)] return self._paginate(data, sort='title') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(identifier, predicate=filter_series) if not series: return self._not_found('Series not found') detailed = self._parse_boolean(self.get_argument('detailed', default=True)) data = series.to_json(detailed=detailed) if path_param: if path_param not in data: return self._bad_request("Invalid path parameter'{0}'".format(path_param)) data = data[path_param] return self._ok(data)
def http_get(self, series_slug, identifier): """Query series information. :param series_slug: series slug. E.g.: tvdb1234 :param identifier: """ series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') if identifier == 'backlogged': # TODO: revisit allowed_qualities = self._parse(self.get_argument('allowed', default=None), str) allowed_qualities = list(map(int, allowed_qualities.split(','))) if allowed_qualities else [] preferred_qualities = self._parse(self.get_argument('preferred', default=None), str) preferred_qualities = list(map(int, preferred_qualities.split(','))) if preferred_qualities else [] new, existing = series.get_backlogged_episodes(allowed_qualities=allowed_qualities, preferred_qualities=preferred_qualities) data = {'new': new, 'existing': existing} return self._ok(data=data) return self._bad_request('Invalid request')
def _search_manual(self, data): """Queue a manual search for results for the provided episodes or season. :param data: :return: """ if not data.get('showSlug'): return self._bad_request('For a manual search you need to provide a show slug') if not data.get('episodes') and not data.get('season'): return self._bad_request('For a manual search you need to provide a list of episodes or seasons') identifier = SeriesIdentifier.from_slug(data['showSlug']) if not identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') episode_segments = self._get_episode_segments(series, data) season_segments = self._get_season_segments(series, data) for segments in ({'segment': episode_segments, 'manual_search_type': 'episode'}, {'segment': season_segments, 'manual_search_type': 'season'}): for segment in itervalues(segments['segment']): cur_manual_search_queue_item = ManualSearchQueueItem(series, segment, manual_search_type=segments['manual_search_type']) app.forced_search_queue_scheduler.action.add_item(cur_manual_search_queue_item) if not episode_segments and not season_segments: return self._not_found('Could not find any episode for show {show}. Did you provide the correct format?' .format(show=series.name)) return self._accepted('Manual search for {0} started'.format(data['showSlug']))
def http_patch(self, series_slug, path_param=None): """Patch series.""" if not series_slug: return self._method_not_allowed('Patching multiple series is not allowed') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') data = json_decode(self.request.body) indexer_id = data.get('id', {}).get(identifier.indexer.slug) if indexer_id is not None and indexer_id != identifier.id: return self._bad_request('Conflicting series identifier') accepted = {} ignored = {} patches = { 'config.aliases': ListField(series, 'aliases'), 'config.defaultEpisodeStatus': StringField(series, 'default_ep_status_name'), 'config.dvdOrder': BooleanField(series, 'dvd_order'), 'config.seasonFolders': BooleanField(series, 'season_folders'), 'config.anime': BooleanField(series, 'anime'), 'config.scene': BooleanField(series, 'scene'), 'config.sports': BooleanField(series, 'sports'), 'config.paused': BooleanField(series, 'paused'), 'config.location': StringField(series, 'location'), 'config.airByDate': BooleanField(series, 'air_by_date'), 'config.subtitlesEnabled': BooleanField(series, 'subtitles'), 'config.release.requiredWords': ListField(series, 'release_required_words'), 'config.release.ignoredWords': ListField(series, 'release_ignore_words'), 'config.release.blacklist': ListField(series, 'blacklist'), 'config.release.whitelist': ListField(series, 'whitelist'), 'config.release.requiredWordsExclude': BooleanField(series, 'rls_require_exclude'), 'config.release.ignoredWordsExclude': BooleanField(series, 'rls_ignore_exclude'), 'language': StringField(series, 'lang'), 'config.qualities.allowed': ListField(series, 'qualities_allowed'), 'config.qualities.preferred': ListField(series, 'qualities_preferred'), 'config.qualities.combined': IntegerField(series, 'quality'), 'config.airdateOffset': IntegerField(series, 'airdate_offset'), } for key, value in iter_nested_items(data): patch_field = patches.get(key) if patch_field and patch_field.patch(series, value): set_nested_value(accepted, key, value) else: set_nested_value(ignored, key, value) # Save patched attributes in db. series.save_to_db() if ignored: log.warning('Series patch ignored {items!r}', {'items': ignored}) return self._ok(data=accepted)
def backlogShow(self, showslug): identifier = SeriesIdentifier.from_slug(showslug) series_obj = Series.find_by_identifier(identifier) if series_obj: app.backlog_search_scheduler.action.search_backlog([series_obj]) return self.redirect('/manage/backlogOverview/')
def patch(self, series_slug, path_param=None): """Patch series.""" if not series_slug: return self._method_not_allowed('Patching multiple series is not allowed') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') data = json_decode(self.request.body) indexer_id = data.get('id', {}).get(identifier.indexer.slug) if indexer_id is not None and indexer_id != identifier.id: return self._bad_request('Conflicting series identifier') accepted = {} ignored = {} patches = { 'config.aliases': ListField(series, 'aliases'), 'config.defaultEpisodeStatus': StringField(series, 'default_ep_status_name'), 'config.dvdOrder': BooleanField(series, 'dvd_order'), 'config.seasonFolders': BooleanField(series, 'season_folders'), 'config.anime': BooleanField(series, 'anime'), 'config.scene': BooleanField(series, 'scene'), 'config.sports': BooleanField(series, 'sports'), 'config.paused': BooleanField(series, 'paused'), 'config.location': StringField(series, '_location'), 'config.airByDate': BooleanField(series, 'air_by_date'), 'config.subtitlesEnabled': BooleanField(series, 'subtitles'), 'config.release.requiredWords': ListField(series, 'release_required_words'), 'config.release.ignoredWords': ListField(series, 'release_ignore_words'), 'config.release.blacklist': ListField(series, 'blacklist'), 'config.release.whitelist': ListField(series, 'whitelist'), 'language': StringField(series, 'lang'), 'config.qualities.allowed': ListField(series, 'qualities_allowed'), 'config.qualities.preferred': ListField(series, 'qualities_preferred'), 'config.qualities.combined': IntegerField(series, 'quality'), } for key, value in iter_nested_items(data): patch_field = patches.get(key) if patch_field and patch_field.patch(series, value): set_nested_value(accepted, key, value) else: set_nested_value(ignored, key, value) # Save patched attributes in db. series.save_to_db() if ignored: log.warning('Series patch ignored {items!r}', {'items': ignored}) self._ok(data=accepted)
def get(self, series_slug, path_param): """ Get history records. History records can be specified using a show slug. """ sql_base = """ SELECT rowid, date, action, quality, provider, version, proper_tags, manually_searched, resource, size, indexer_id, showid, season, episode FROM history """ params = [] arg_page = self._get_page() arg_limit = self._get_limit(default=50) if series_slug is not None: series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series') sql_base += ' WHERE indexer_id = ? AND showid = ?' params += [series_identifier.indexer.id, series_identifier.id] sql_base += ' ORDER BY date DESC' results = db.DBConnection().select(sql_base, params) def data_generator(): """Read and paginate history records.""" start = arg_limit * (arg_page - 1) for item in results[start:start + arg_limit]: d = {} d['id'] = item['rowid'] d['series'] = SeriesIdentifier.from_id(item['indexer_id'], item['showid']).slug d['status'] = item['action'] d['actionDate'] = item['date'] d['resource'] = basename(item['resource']) d['size'] = item['size'] d['properTags'] = item['proper_tags'] d['statusName'] = statusStrings.get(item['action']) d['season'] = item['season'] d['episode'] = item['episode'] d['manuallySearched'] = bool(item['manually_searched']) d['provider'] = item['provider'] yield d if not results: return self._not_found('History data not found') return self._paginate(data_generator=data_generator)
def resource_search_missing_subtitles(self): """ Search missing subtitles for multiple episodes at once. example: Pass the following structure: language: 'all', // Or a three letter language code. shows: [ { 'slug': 'tvdb1234', 'episodes': ['s01e01', 's02e03', 's10e10'] }, ] """ data = json_decode(self.request.body) language = data.get('language', 'all') shows = data.get('shows', []) if language != 'all' and language not in subtitle_code_filter(): return self._bad_request( 'You need to provide a valid subtitle code') for show in shows: # Loop through the shows. Each show should have an array of episode slugs series_identifier = SeriesIdentifier.from_slug(show.get('slug')) if not series_identifier: log.warning( 'Could not create a show identifier with slug {slug}', {'slug': show.get('slug')}) continue series = Series.find_by_identifier(series_identifier) if not series: log.warning( 'Could not match to a show in the library with slug {slug}', {'slug': show.get('slug')}) continue for episode_slug in show.get('episodes', []): episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: log.warning('Bad episode number from slug {slug}', {'slug': episode_slug}) continue episode = Episode.find_by_series_and_episode( series, episode_number) if not episode: log.warning('Episode not found with slug {slug}', {'slug': episode_slug}) episode.download_subtitles( lang=language if language != 'all' else None) return self._ok()
def _search_failed(self, data): """Queue a failed search. :param data: :return: """ statuses = {} if not data.get('showSlug'): return self._bad_request( 'For a failed search you need to provide a show slug') if not data.get('episodes'): return self._bad_request( 'For a failed search you need to provide a list of episodes') identifier = SeriesIdentifier.from_slug(data['showSlug']) if not identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') season_segments = defaultdict(list) for episode_slug in data['episodes']: episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: statuses[episode_slug] = {'status': 400} continue episode = Episode.find_by_series_and_episode( series, episode_number) if not episode: statuses[episode_slug] = {'status': 404} continue season_segments[episode.season].append(episode) if not season_segments: return self._not_found( 'Could not find any episode for show {show}. Did you provide the correct format?' .format(show=series.name)) for segment in itervalues(season_segments): cur_failed_queue_item = FailedQueueItem(series, segment) app.forced_search_queue_scheduler.action.add_item( cur_failed_queue_item) return self._accepted('Failed search for {0} started'.format( data['showSlug']))
def togglePause(self, showslug=None): # @TODO: Replace with PUT to update the state var /api/v2/show/{id} identifier = SeriesIdentifier.from_slug(showslug) error, series_obj = Show.pause(identifier.indexer.slug, identifier.id) if error is not None: return self._genericMessage('Error', error) ui.notifications.message('{show} has been {state}'.format( show=series_obj.name, state='paused' if series_obj.paused else 'resumed')) return self.redirect( '/home/displayShow?showslug={series_obj.slug}'.format( series_obj=series_obj))
def searchEpisodeSubtitles(self, showslug=None, season=None, episode=None, lang=None): # retrieve the episode object and fail if we can't get one series_obj = Series.find_by_identifier( SeriesIdentifier.from_slug(showslug)) ep_obj = series_obj.get_episode(season, episode) if not ep_obj: return json.dumps({ 'result': 'failure', }) try: if lang: logger.log( 'Manual re-downloading subtitles for {show} with language {lang}' .format(show=ep_obj.series.name, lang=lang)) new_subtitles = ep_obj.download_subtitles(lang=lang) except Exception as error: return json.dumps({ 'result': 'failure', 'description': 'Error while downloading subtitles: {error}'.format( error=error) }) if new_subtitles: new_languages = [ subtitles.name_from_code(code) for code in new_subtitles ] description = 'New subtitles downloaded: {languages}'.format( languages=', '.join(new_languages)) result = 'success' else: new_languages = [] description = 'No subtitles downloaded' result = 'failure' ui.notifications.message(ep_obj.series.name, description) return json.dumps({ 'result': result, 'subtitles': ep_obj.subtitles, 'languages': new_languages, 'description': description })
def get(self, series_slug, identifier, *args, **kwargs): """Get an asset.""" series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') asset_type = identifier or 'banner' asset = series.get_asset(asset_type) if not asset: return self._not_found('Asset not found') self._ok(stream=asset.media, content_type=asset.media_type)
def get(self, series_slug, episode_slug, path_param): """Query episode information. :param series_slug: series slug. E.g.: tvdb1234 :param episode_number: :param path_param: """ series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') if not episode_slug: detailed = self._parse_boolean( self.get_argument('detailed', default=False)) season = self._parse(self.get_argument('season', None), int) data = [ e.to_json(detailed=detailed) for e in series.get_all_episodes(season=season) ] return self._paginate(data, sort='airDate') episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: return self._bad_request('Invalid episode number') episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: return self._not_found('Episode not found') detailed = self._parse_boolean( self.get_argument('detailed', default=True)) data = episode.to_json(detailed=detailed) if path_param: if path_param == 'metadata': data = episode.metadata() if episode.is_location_valid( ) else {} elif path_param in data: data = data[path_param] else: return self._bad_request( "Invalid path parameter '{0}'".format(path_param)) return self._ok(data=data)
def delete(self, series_slug, path_param=None): """Delete the series.""" if not series_slug: return self._method_not_allowed('Deleting multiple series are not allowed') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') remove_files = self._parse_boolean(self.get_argument('remove-files', default=None)) if not series.delete(remove_files): return self._conflict('Unable to delete series') return self._no_content()
def get(self, identifier): """Collect ran, running and queued searches for a specific show. :param identifier: """ if not identifier: return self._bad_request( 'You need to add the show slug to the route') series = SeriesIdentifier.from_slug(identifier) if not series: return self._bad_request('Invalid series slug') series_obj = Series.find_by_identifier(series) if not series_obj: return self._not_found('Series not found') return {'results': collect_episodes_from_search_thread(series_obj)}
def http_delete(self, series_slug, path_param=None): """Delete the series.""" if not series_slug: return self._method_not_allowed('Deleting multiple series are not allowed') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') remove_files = self._parse_boolean(self.get_argument('remove-files', default=None)) if not series.delete(remove_files): return self._conflict('Unable to delete series') return self._no_content()
def test_series_identifier(p): # Given slug = p['slug'] expected = p['expected'] # When actual = SeriesIdentifier.from_slug(slug) # Then if expected is None: assert actual is None else: assert actual assert expected == actual assert expected.id == actual.id assert expected.indexer == actual.indexer assert expected.id != actual assert expected.indexer != actual
def test_series_identifier(p): # Given slug = p['slug'] expected = p['expected'] # When actual = SeriesIdentifier.from_slug(slug) # Then if expected is None: assert actual is None else: assert actual assert expected == actual assert expected.id == actual.id assert expected.indexer == actual.indexer assert expected.id != actual assert expected.indexer != actual
def patch(self, series_slug, path_param=None): """Patch series.""" if not series_slug: return self._method_not_allowed( 'Patching multiple series is not allowed') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series identifier') series = Series.find_by_identifier(identifier) if not series: return self._not_found('Series not found') data = json_decode(self.request.body) indexer_id = data.get('id', {}).get(identifier.indexer.slug) if indexer_id is not None and indexer_id != identifier.id: return self._bad_request('Conflicting series identifier') accepted = {} ignored = {} patches = { 'config.dvdOrder': BooleanField(series, 'dvd_order'), 'config.flattenFolders': BooleanField(series, 'flatten_folders'), 'config.scene': BooleanField(series, 'scene'), 'config.paused': BooleanField(series, 'paused'), 'config.location': StringField(series, '_location'), 'config.airByDate': BooleanField(series, 'air_by_date'), 'config.subtitlesEnabled': BooleanField(series, 'subtitles') } for key, value in iter_nested_items(data): patch_field = patches.get(key) if patch_field and patch_field.patch(series, value): set_nested_value(accepted, key, value) else: set_nested_value(ignored, key, value) # Save patched attributes in db. series.save_to_db() if ignored: log.warning('Series patch ignored %r', ignored) self._ok(data=accepted)
def refreshShow(self, showslug=None): # @TODO: Replace with status=refresh from PATCH /api/v2/show/{id} identifier = SeriesIdentifier.from_slug(showslug) error, series_obj = Show.refresh(identifier.indexer.slug, identifier.id) # This is a show validation error if error is not None and series_obj is None: return self._genericMessage('Error', error) # This is a refresh error if error is not None: ui.notifications.error('Unable to refresh this show.', error) time.sleep(cpu_presets[app.CPU_PRESET]) return self.redirect( '/home/displayShow?showslug={series_obj.slug}'.format( series_obj=series_obj))
def subtitleShow(self, showslug=None): if showslug is None: return self._genericMessage('Error', 'Invalid show ID') identifier = SeriesIdentifier.from_slug(showslug) series_obj = Series.find_by_identifier(identifier) if series_obj is None: return self._genericMessage('Error', 'Unable to find the specified show') # search and download subtitles app.show_queue_scheduler.action.download_subtitles(series_obj) time.sleep(cpu_presets[app.CPU_PRESET]) return self.redirect( '/home/displayShow?showslug={series_obj.slug}'.format( series_obj=series_obj))
def http_get(self, series_slug, identifier, *args, **kwargs): """Get an asset.""" series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') asset_type = identifier or 'banner' asset = series.get_asset(asset_type, fallback=False) if not asset: return self._not_found('Asset not found') media = asset.media if not media: return self._not_found('{kind} not found'.format(kind=asset_type.capitalize())) return self._ok(stream=media, content_type=asset.media_type)
def http_get(self, series_slug, identifier, *args, **kwargs): """Get an asset.""" series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') asset_type = identifier or 'banner' asset = series.get_asset(asset_type, fallback=False) if not asset: return self._not_found('Asset not found') media = asset.media if not media: return self._not_found( '{kind} not found'.format(kind=asset_type.capitalize())) return self._ok(stream=media, content_type=asset.media_type)
def http_get(self, series_slug, episode_slug, path_param): """Query episode information. :param series_slug: series slug. E.g.: tvdb1234 :param episode_number: :param path_param: """ series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') if not episode_slug: detailed = self._parse_boolean(self.get_argument('detailed', default=False)) season = self._parse(self.get_argument('season', None), int) data = [e.to_json(detailed=detailed) for e in series.get_all_episodes(season=season)] return self._paginate(data, sort='airDate') episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: return self._bad_request('Invalid episode number') episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: return self._not_found('Episode not found') detailed = self._parse_boolean(self.get_argument('detailed', default=True)) data = episode.to_json(detailed=detailed) if path_param: if path_param == 'metadata': data = episode.metadata() if episode.is_location_valid() else {} elif path_param in data: data = data[path_param] else: return self._bad_request("Invalid path parameter '{0}'".format(path_param)) return self._ok(data=data)
def put(self, identifier, **kwargs): """Update alias information.""" identifier = self._parse(identifier) if not identifier: return self._not_found('Invalid alias id') data = json_decode(self.request.body) if not data or not all([ data.get('id'), data.get('series'), data.get('name'), data.get('type') ]) or data['id'] != identifier: return self._bad_request('Invalid request body') series_identifier = SeriesIdentifier.from_slug(data.get('series')) if not series_identifier: return self._bad_request('Invalid series') cache_db_con = db.DBConnection('cache.db') last_changes = cache_db_con.connection.total_changes cache_db_con.action( b'UPDATE scene_exceptions' b' set indexer = ?' b', indexer_id = ?' b', show_name = ?' b', season = ?' b', custom = 1' b' WHERE exception_id = ?', [ series_identifier.indexer.id, series_identifier.id, data['name'], data.get('season'), identifier ]) if cache_db_con.connection.total_changes - last_changes != 1: return self._not_found('Alias not found') return self._no_content()
def emby_update(self): """Update emby's show library.""" show_slug = self.get_argument('showslug', '') show = None if show_slug: show_identifier = SeriesIdentifier.from_slug(show_slug) if not show_identifier: return self._bad_request('Invalid show slug') show = Series.find_by_identifier(show_identifier) if not show: return self._not_found('Series not found') if notifiers.emby_notifier.update_library(show): ui.notifications.message( f'Library update command sent to Emby host: {app.EMBY_HOST}') else: ui.notifications.error( f'Unable to contact Emby host: {app.EMBY_HOST}') return self._created()
def getSeasonSceneExceptions(self, showslug=None): """Get show name scene exceptions per season :param indexer: The shows indexer :param indexer_id: The shows indexer_id :return: A json with the scene exceptions per season. """ identifier = SeriesIdentifier.from_slug(showslug) series_obj = Series.find_by_identifier(identifier) return json.dumps({ 'seasonExceptions': { season: list(exception_name) for season, exception_name in iteritems( get_all_scene_exceptions(series_obj)) }, 'xemNumbering': { tvdb_season_ep[0]: anidb_season_ep[0] for (tvdb_season_ep, anidb_season_ep) in iteritems( get_xem_numbering_for_show(series_obj, refresh_data=False)) } })
def post(self, series_slug): """Query series information. :param series_slug: series slug. E.g.: tvdb1234 """ series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') data = json_decode(self.request.body) if not data or not all([data.get('type')]) or len(data) != 1: return self._bad_request('Invalid request body') if data['type'] == 'ARCHIVE_EPISODES': if series.set_all_episodes_archived(final_status_only=True): return self._created() return self._no_content() return self._bad_request('Invalid operation')
def http_post(self, series_slug): """Query series information. :param series_slug: series slug. E.g.: tvdb1234 """ series_identifier = SeriesIdentifier.from_slug(series_slug) if not series_identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(series_identifier) if not series: return self._not_found('Series not found') data = json_decode(self.request.body) if not data or not all([data.get('type')]) or len(data) != 1: return self._bad_request('Invalid request body') if data['type'] == 'ARCHIVE_EPISODES': if series.set_all_episodes_archived(final_status_only=True): return self._created() return self._no_content() return self._bad_request('Invalid operation')
def saveShowNotifyList(show=None, emails=None, prowlAPIs=None): series_identifier = SeriesIdentifier.from_slug(show) series_obj = Series.find_by_identifier(series_identifier) # Create a new dict, to force the "dirty" flag on the Series object. entries = {'emails': '', 'prowlAPIs': ''} if not series_obj: return 'show missing' if series_obj.notify_list: entries.update(series_obj.notify_list) if emails is not None: entries['emails'] = emails if prowlAPIs is not None: entries['prowlAPIs'] = prowlAPIs series_obj.notify_list = entries series_obj.save_to_db() return 'OK'
def http_get(self, series_slug, path_param=None): """Query series information. :param series_slug: series slug. E.g.: tvdb1234 :param path_param: """ arg_paused = self._parse_boolean(self.get_argument('paused', default=None)) def filter_series(current): return arg_paused is None or current.paused == arg_paused if not series_slug: detailed = self._parse_boolean(self.get_argument('detailed', default=False)) fetch = self._parse_boolean(self.get_argument('fetch', default=False)) data = [ s.to_json(detailed=detailed, fetch=fetch) for s in Series.find_series(predicate=filter_series) ] return self._paginate(data, sort='title') identifier = SeriesIdentifier.from_slug(series_slug) if not identifier: return self._bad_request('Invalid series slug') series = Series.find_by_identifier(identifier, predicate=filter_series) if not series: return self._not_found('Series not found') detailed = self._parse_boolean(self.get_argument('detailed', default=True)) fetch = self._parse_boolean(self.get_argument('fetch', default=False)) data = series.to_json(detailed=detailed, fetch=fetch) if path_param: if path_param not in data: return self._bad_request("Invalid path parameter '{0}'".format(path_param)) data = data[path_param] return self._ok(data)
def massEdit(self, toEdit=None): t = PageTemplate(rh=self, filename='manage_massEdit.mako') if not toEdit: return self.redirect('/manage/') series_slugs = toEdit.split('|') show_list = [] show_names = [] for slug in series_slugs: identifier = SeriesIdentifier.from_slug(slug) series_obj = Series.find_by_identifier(identifier) if series_obj: show_list.append(series_obj) show_names.append(series_obj.name) season_folders_all_same = True last_season_folders = None paused_all_same = True last_paused = None default_ep_status_all_same = True last_default_ep_status = None anime_all_same = True last_anime = None sports_all_same = True last_sports = None quality_all_same = True last_quality = None subtitles_all_same = True last_subtitles = None scene_all_same = True last_scene = None air_by_date_all_same = True last_air_by_date = None dvd_order_all_same = True last_dvd_order = None root_dir_list = [] for cur_show in show_list: cur_root_dir = os.path.dirname(cur_show._location) # pylint: disable=protected-access if cur_root_dir not in root_dir_list: root_dir_list.append(cur_root_dir) # if we know they're not all the same then no point even bothering if paused_all_same: # if we had a value already and this value is different then they're not all the same if last_paused not in (None, cur_show.paused): paused_all_same = False else: last_paused = cur_show.paused if default_ep_status_all_same: if last_default_ep_status not in (None, cur_show.default_ep_status): default_ep_status_all_same = False else: last_default_ep_status = cur_show.default_ep_status if anime_all_same: # if we had a value already and this value is different then they're not all the same if last_anime not in (None, cur_show.is_anime): anime_all_same = False else: last_anime = cur_show.anime if season_folders_all_same: if last_season_folders not in (None, cur_show.season_folders): season_folders_all_same = False else: last_season_folders = cur_show.season_folders if quality_all_same: if last_quality not in (None, cur_show.quality): quality_all_same = False else: last_quality = cur_show.quality if subtitles_all_same: if last_subtitles not in (None, cur_show.subtitles): subtitles_all_same = False else: last_subtitles = cur_show.subtitles if scene_all_same: if last_scene not in (None, cur_show.scene): scene_all_same = False else: last_scene = cur_show.scene if sports_all_same: if last_sports not in (None, cur_show.sports): sports_all_same = False else: last_sports = cur_show.sports if air_by_date_all_same: if last_air_by_date not in (None, cur_show.air_by_date): air_by_date_all_same = False else: last_air_by_date = cur_show.air_by_date if dvd_order_all_same: if last_dvd_order not in (None, cur_show.dvd_order): dvd_order_all_same = False else: last_dvd_order = cur_show.dvd_order default_ep_status_value = last_default_ep_status if default_ep_status_all_same else None paused_value = last_paused if paused_all_same else None anime_value = last_anime if anime_all_same else None season_folders_value = last_season_folders if season_folders_all_same else None quality_value = last_quality if quality_all_same else None subtitles_value = last_subtitles if subtitles_all_same else None scene_value = last_scene if scene_all_same else None sports_value = last_sports if sports_all_same else None air_by_date_value = last_air_by_date if air_by_date_all_same else None dvd_order_value = last_dvd_order if dvd_order_all_same else None root_dir_list = root_dir_list return t.render(showList=toEdit, showNames=show_names, default_ep_status_value=default_ep_status_value, dvd_order_value=dvd_order_value, paused_value=paused_value, anime_value=anime_value, season_folders_value=season_folders_value, quality_value=quality_value, subtitles_value=subtitles_value, scene_value=scene_value, sports_value=sports_value, air_by_date_value=air_by_date_value, root_dir_list=root_dir_list)
def massEditSubmit(self, paused=None, default_ep_status=None, dvd_order=None, anime=None, sports=None, scene=None, season_folders=None, quality_preset=None, subtitles=None, air_by_date=None, allowed_qualities=None, preferred_qualities=None, toEdit=None, *args, **kwargs): allowed_qualities = allowed_qualities or [] preferred_qualities = preferred_qualities or [] dir_map = {} for cur_arg in kwargs: if not cur_arg.startswith('orig_root_dir_'): continue which_index = cur_arg.replace('orig_root_dir_', '') end_dir = kwargs['new_root_dir_{index}'.format(index=which_index)] dir_map[kwargs[cur_arg]] = end_dir series_slugs = toEdit.split('|') if toEdit else [] errors = 0 for series_slug in series_slugs: identifier = SeriesIdentifier.from_slug(series_slug) series_obj = Series.find_by_identifier(identifier) if not series_obj: continue cur_root_dir = os.path.dirname(series_obj._location) cur_show_dir = os.path.basename(series_obj._location) if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]: new_show_dir = os.path.join(dir_map[cur_root_dir], cur_show_dir) logger.log(u'For show {show.name} changing dir from {show._location} to {location}'.format (show=series_obj, location=new_show_dir)) else: new_show_dir = series_obj._location if paused == 'keep': new_paused = series_obj.paused else: new_paused = True if paused == 'enable' else False new_paused = 'on' if new_paused else 'off' if default_ep_status == 'keep': new_default_ep_status = series_obj.default_ep_status else: new_default_ep_status = default_ep_status if anime == 'keep': new_anime = series_obj.anime else: new_anime = True if anime == 'enable' else False new_anime = 'on' if new_anime else 'off' if sports == 'keep': new_sports = series_obj.sports else: new_sports = True if sports == 'enable' else False new_sports = 'on' if new_sports else 'off' if scene == 'keep': new_scene = series_obj.is_scene else: new_scene = True if scene == 'enable' else False new_scene = 'on' if new_scene else 'off' if air_by_date == 'keep': new_air_by_date = series_obj.air_by_date else: new_air_by_date = True if air_by_date == 'enable' else False new_air_by_date = 'on' if new_air_by_date else 'off' if dvd_order == 'keep': new_dvd_order = series_obj.dvd_order else: new_dvd_order = True if dvd_order == 'enable' else False new_dvd_order = 'on' if new_dvd_order else 'off' if season_folders == 'keep': new_season_folders = series_obj.season_folders else: new_season_folders = True if season_folders == 'enable' else False new_season_folders = 'on' if new_season_folders else 'off' if subtitles == 'keep': new_subtitles = series_obj.subtitles else: new_subtitles = True if subtitles == 'enable' else False new_subtitles = 'on' if new_subtitles else 'off' if quality_preset == 'keep': allowed_qualities, preferred_qualities = series_obj.current_qualities elif try_int(quality_preset, None): preferred_qualities = [] exceptions_list = [] errors += self.editShow(identifier.indexer.slug, identifier.id, new_show_dir, allowed_qualities, preferred_qualities, exceptions_list, defaultEpStatus=new_default_ep_status, season_folders=new_season_folders, paused=new_paused, sports=new_sports, dvd_order=new_dvd_order, subtitles=new_subtitles, anime=new_anime, scene=new_scene, air_by_date=new_air_by_date, directCall=True) if errors: ui.notifications.error('Errors', '{num} error{s} while saving changes. Please check logs'.format (num=errors, s='s' if errors > 1 else '')) return self.redirect('/manage/')
def massUpdate(self, toUpdate=None, toRefresh=None, toRename=None, toDelete=None, toRemove=None, toMetadata=None, toSubtitle=None, toImageUpdate=None): to_update = toUpdate.split('|') if toUpdate else [] to_refresh = toRefresh.split('|') if toRefresh else [] to_rename = toRename.split('|') if toRename else [] to_subtitle = toSubtitle.split('|') if toSubtitle else [] to_delete = toDelete.split('|') if toDelete else [] to_remove = toRemove.split('|') if toRemove else [] to_metadata = toMetadata.split('|') if toMetadata else [] to_image_update = toImageUpdate.split('|') if toImageUpdate else [] errors = [] refreshes = [] updates = [] renames = [] subtitles = [] image_update = [] for slug in set(to_update + to_refresh + to_rename + to_subtitle + to_delete + to_remove + to_metadata + to_image_update): identifier = SeriesIdentifier.from_slug(slug) series_obj = Series.find_by_identifier(identifier) if not series_obj: continue if slug in to_delete + to_remove: app.show_queue_scheduler.action.removeShow(series_obj, slug in to_delete) continue # don't do anything else if it's being deleted or removed if slug in to_update: try: app.show_queue_scheduler.action.updateShow(series_obj) updates.append(series_obj.name) except CantUpdateShowException as msg: errors.append('Unable to update show: {error}'.format(error=msg)) elif slug in to_refresh: # don't bother refreshing shows that were updated try: app.show_queue_scheduler.action.refreshShow(series_obj) refreshes.append(series_obj.name) except CantRefreshShowException as msg: errors.append('Unable to refresh show {show.name}: {error}'.format (show=series_obj, error=msg)) if slug in to_rename: app.show_queue_scheduler.action.renameShowEpisodes(series_obj) renames.append(series_obj.name) if slug in to_subtitle: app.show_queue_scheduler.action.download_subtitles(series_obj) subtitles.append(series_obj.name) if slug in to_image_update: image_cache.replace_images(series_obj) if errors: ui.notifications.error('Errors encountered', '<br />\n'.join(errors)) message = '' if updates: message += '\nUpdates: {0}'.format(len(updates)) if refreshes: message += '\nRefreshes: {0}'.format(len(refreshes)) if renames: message += '\nRenames: {0}'.format(len(renames)) if subtitles: message += '\nSubtitles: {0}'.format(len(subtitles)) if image_update: message += '\nImage updates: {0}'.format(len(image_update)) if message: ui.notifications.message('Queued actions:', message) return self.redirect('/manage/')
def http_get(self, identifier, path_param): """Query scene_exception information.""" cache_db_con = db.DBConnection('cache.db') sql_base = ('SELECT ' ' exception_id, ' ' indexer, ' ' indexer_id, ' ' show_name, ' ' season, ' ' custom ' 'FROM scene_exceptions ') sql_where = [] params = [] if identifier is not None: sql_where.append('exception_id') params += [identifier] else: series_slug = self.get_query_argument('series', None) series_identifier = SeriesIdentifier.from_slug(series_slug) if series_slug and not series_identifier: return self._bad_request('Invalid series') season = self._parse(self.get_query_argument('season', None)) exception_type = self.get_query_argument('type', None) if exception_type and exception_type not in ('local', ): return self._bad_request('Invalid type') if series_identifier: sql_where.append('indexer') sql_where.append('indexer_id') params += [series_identifier.indexer.id, series_identifier.id] if season is not None: sql_where.append('season') params += [season] if exception_type == 'local': sql_where.append('custom') params += [1] if sql_where: sql_base += ' WHERE ' + ' AND '.join([where + ' = ? ' for where in sql_where]) sql_results = cache_db_con.select(sql_base, params) data = [] for item in sql_results: d = {} d['id'] = item['exception_id'] d['series'] = SeriesIdentifier.from_id(item['indexer'], item['indexer_id']).slug d['name'] = item['show_name'] d['season'] = item['season'] if item['season'] >= 0 else None d['type'] = 'local' if item['custom'] else None data.append(d) if not identifier: return self._paginate(data, sort='id') if not data: return self._not_found('Alias not found') data = data[0] if path_param: if path_param not in data: return self._bad_request('Invalid path parameter') data = data[path_param] return self._ok(data=data)