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 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_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 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 test_episode_identifier(p): # Given slug = p['slug'] expected = p['expected'] # When actual = EpisodeNumber.from_slug(slug) # Then if expected is None: assert not actual else: assert actual assert expected == actual assert slug != actual
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 _get_episode_segments(series, data): """ Create a dict with season number keys and their corresponding episodes as an array of Episode objects. The episode objects are created from the "episodes" property passed as json data. """ episode_segments = defaultdict(list) if data.get('episodes'): for episode_slug in data['episodes']: episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: continue episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: continue episode_segments[episode.season].append(episode) return episode_segments
def _patch_multi(self, series, request_data): """Patch multiple episodes.""" statuses = {} for slug, data in iteritems(request_data): episode_number = EpisodeNumber.from_slug(slug) if not episode_number: statuses[slug] = {'status': 400} continue episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: statuses[slug] = {'status': 404} continue self._patch_episode(episode, data) statuses[slug] = {'status': 200} return self._multi_status(data=statuses)
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 _get_season_segments(series, data): """ Create a dict with season number keys and their corresponding episodes as an array of Episode objects. The episode objects are created from the "season" property passed as json data. """ season_segments = defaultdict(list) if data.get('season'): for season_slug in data['season']: # For season packs we still need to provide an episode object. So we choose to provide the first episode_slug = '{season}e01'.format(season=season_slug) episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: continue episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: continue season_segments[episode.season].append(episode) return season_segments
def get(self, series_slug, episode_slug, path_param): """Query episode's history information. :param series_slug: series slug. E.g.: tvdb1234 :param episode_slug: episode slug. E.g.: s01e01 :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: return self._bad_request('Invalid episode slug') episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: return self._not_found('Invalid episode number') episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: return self._not_found('Episode not found') sql_base = """ SELECT rowid, date, action, quality, provider, version, resource, size, proper_tags, indexer_id, showid, season, episode, manually_searched, info_hash FROM history WHERE showid = ? AND indexer_id = ? AND season = ? AND episode = ? """ params = [ series.series_id, series.indexer, episode.season, episode.episode ] sql_base += ' ORDER BY date DESC' results = db.DBConnection().select(sql_base, params) def data_generator(): """Read history data and normalize key/value pairs.""" for item in results: provider = {} release_group = None release_name = None file_name = None subtitle_language = None if item['action'] in (SNATCHED, FAILED): provider.update({ 'id': GenericProvider.make_id(item['provider']), 'name': item['provider'] }) release_name = item['resource'] if item['action'] == DOWNLOADED: release_group = item['provider'] file_name = item['resource'] if item['action'] == SUBTITLED: subtitle_language = item['resource'] provider.update({ 'id': item['provider'], 'name': item['provider'] }) if item['action'] == SUBTITLED: subtitle_language = item['resource'] yield { 'id': item['rowid'], 'series': SeriesIdentifier.from_id(item['indexer_id'], item['showid']).slug, 'status': item['action'], 'statusName': statusStrings.get(item['action']), 'actionDate': item['date'], 'quality': item['quality'], 'resource': basename(item['resource']), 'size': item['size'], 'properTags': item['proper_tags'], 'season': item['season'], 'episode': item['episode'], 'manuallySearched': bool(item['manually_searched']), 'infoHash': item['info_hash'], 'provider': provider, 'release_name': release_name, 'releaseGroup': release_group, 'fileName': file_name, 'subtitleLanguage': subtitle_language } if not results: return self._not_found( 'History data not found for show {show} and episode {episode}'. format(show=series.identifier.slug, episode=episode.slug)) return self._ok(data=list(data_generator()))
def get(self, series_slug, episode_slug, path_param): """Query episode's history information. :param series_slug: series slug. E.g.: tvdb1234 :param episode_slug: episode slug. E.g.: s01e01 :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: return self._bad_request('Invalid episode slug') episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: return self._not_found('Invalid episode number') episode = Episode.find_by_series_and_episode(series, episode_number) if not episode: return self._not_found('Episode not found') sql_base = """ SELECT rowid, date, action, quality, provider, version, resource, size, proper_tags, indexer_id, showid, season, episode, manually_searched FROM history WHERE showid = ? AND indexer_id = ? AND season = ? AND episode = ? """ params = [ series.series_id, series.indexer, episode.season, episode.episode ] sql_base += ' ORDER BY date DESC' results = db.DBConnection().select(sql_base, params) def data_generator(): """Read history data and normalize key/value pairs.""" for item in results: 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 for show {show} and episode {episode}'. format(show=series.identifier.slug, episode=episode.slug)) return self._ok(data=list(data_generator()))
def resource_update_episode_status(self): """ Mass update episodes statuses for multiple shows at once. example: Pass the following structure: status: 3, shows: [ { 'slug': 'tvdb1234', 'episodes': ['s01e01', 's02e03', 's10e10'] }, ] """ data = json_decode(self.request.body) status = data.get('status') shows = data.get('shows', []) if status not in statusStrings: return self._bad_request('You need to provide a valid status code') ep_sql_l = [] 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 episodes = [] 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}) ep_sql = episode.mass_update_episode_status(status) if ep_sql: ep_sql_l.append(ep_sql) # Keep an array of episodes for the trakt sync episodes.append(episode) if episodes: series.sync_trakt_episodes(status, episodes) if ep_sql_l: main_db_con = db.DBConnection() main_db_con.mass_action(ep_sql_l) return self._ok(data={'count': len(ep_sql_l)})
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')]): 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() if data['type'] == 'TEST_RENAME': try: series.validate_location # @UnusedVariable except ShowDirectoryNotFoundException: return self._bad_request( "Can't rename episodes when the show dir is missing.") ep_obj_list = series.get_all_episodes(has_location=True) ep_obj_list = [x for x in ep_obj_list if x.location] ep_obj_rename_list = [] for ep_obj in ep_obj_list: has_already = False for check in ep_obj.related_episodes + [ep_obj]: if check in ep_obj_rename_list: has_already = True break if not has_already: ep_obj_rename_list.append(ep_obj) if ep_obj_rename_list: ep_obj_rename_list.reverse() return self._ok(data=[{ **ep_obj.to_json(detailed=True), **{ 'selected': False } } for ep_obj in ep_obj_rename_list]) if data['type'] == 'RENAME_EPISODES': episodes = data.get('episodes', []) if not episodes: return self._bad_request( 'You must provide at least one episode') try: series.validate_location # @UnusedVariable except ShowDirectoryNotFoundException: return self._bad_request( "Can't rename episodes when the show dir is missing.") main_db_con = db.DBConnection() for episode_slug in episodes: episode_number = EpisodeNumber.from_slug(episode_slug) if not episode_number: continue episode = Episode.find_by_series_and_episode( series, episode_number) if not episode: continue # this is probably the worst possible way to deal with double eps # but I've kinda painted myself into a corner here with this stupid database ep_result = main_db_con.select( 'SELECT location ' 'FROM tv_episodes ' 'WHERE indexer = ? AND showid = ? AND season = ? AND episode = ? AND 5=5', [ series.indexer, series.series_id, episode.season, episode.episode ]) if not ep_result: log.warning( 'Unable to find an episode for {episode}, skipping', {'episode': episode}) continue related_eps_result = main_db_con.select( 'SELECT season, episode ' 'FROM tv_episodes ' 'WHERE location = ? AND episode != ?', [ep_result[0]['location'], episode.episode]) root_ep_obj = episode root_ep_obj.related_episodes = [] for cur_related_ep in related_eps_result: related_ep_obj = series.get_episode( cur_related_ep['season'], cur_related_ep['episode']) if related_ep_obj not in root_ep_obj.related_episodes: root_ep_obj.related_episodes.append(related_ep_obj) root_ep_obj.rename() return self._created() # This might also be moved to /notifications/kodi/update?showslug=.. if data['type'] == 'UPDATE_KODI': series_name = quote_plus(series.name.encode('utf-8')) if app.KODI_UPDATE_ONLYFIRST: host = app.KODI_HOST[0].strip() else: host = ', '.join(app.KODI_HOST) if notifiers.kodi_notifier.update_library(series_name=series_name): ui.notifications.message( f'Library update command sent to KODI host(s): {host}') else: ui.notifications.error( f'Unable to contact one or more KODI host(s): {host}') return self._created() return self._bad_request('Invalid operation')