Commit 3b1f1e3b authored by echel0n's avatar echel0n
Browse files

Refactored API v2 error handling and async functionality

Fixed issues with API v2 post-processing
parent 4a7fdbb6
......@@ -22,7 +22,9 @@ import functools
import json
import traceback
import types
from concurrent.futures.thread import ThreadPoolExecutor
import bleach
import sentry_sdk
from apispec import APISpec
from apispec.exceptions import APISpecError
......@@ -31,14 +33,18 @@ from apispec_webframeworks.tornado import TornadoPlugin
from tornado.escape import to_basestring
from tornado.ioloop import IOLoop
from tornado.web import HTTPError
from tornado.web import RequestHandler
import sickrage
from sickrage.core.enums import UserPermission
from sickrage.core.helpers import get_internal_ip
from sickrage.core.webserver.handlers.base import BaseHandler
class APIBaseHandler(BaseHandler):
class APIBaseHandler(RequestHandler):
def __init__(self, application, request, api_version='', **kwargs):
super(APIBaseHandler, self).__init__(application, request, **kwargs)
self.executor = ThreadPoolExecutor(thread_name_prefix=f'API{api_version}-Thread')
def prepare(self):
super(APIBaseHandler, self).prepare()
......@@ -84,7 +90,7 @@ class APIBaseHandler(BaseHandler):
})
if sickrage.app.config.user.sub_id != decoded_token.get('sub'):
return self.send_error(401, error='user is not authorized')
return self._unauthorized(error='user is not authorized')
if not sickrage.app.api.token:
exchanged_token = sickrage.app.auth_server.token_exchange(token)
......@@ -110,11 +116,11 @@ class APIBaseHandler(BaseHandler):
method = self.run_async(getattr(self, method_name))
setattr(self, method_name, method)
except Exception:
return self.send_error(401, error='failed to decode token')
return self._unauthorized(error='failed to decode token')
else:
return self.send_error(401, error='invalid authorization request')
return self._unauthorized(error='invalid authorization request')
else:
return self.send_error(401, error='authorization header missing')
return self._unauthorized(error='authorization header missing')
def run_async(self, method):
@functools.wraps(method)
......@@ -134,9 +140,6 @@ class APIBaseHandler(BaseHandler):
return decoded_token
def write_error(self, status_code, **kwargs):
self.set_header('Content-Type', 'application/json')
self.set_status(status_code)
if status_code == 500:
excp = kwargs['exc_info'][1]
tb = kwargs['exc_info'][2]
......@@ -148,14 +151,42 @@ class APIBaseHandler(BaseHandler):
sickrage.app.log.error(error_msg)
return self.finish(self.to_json({'error': error_msg}))
return self.finish(self.json_response(error=error_msg, status=status_code))
def set_default_headers(self):
super(APIBaseHandler, self).set_default_headers()
self.set_header('X-SiCKRAGE-Server', sickrage.version())
self.set_header("Access-Control-Allow-Origin", "*")
self.set_header("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With, X-SiCKRAGE-Server")
self.set_header('Access-Control-Allow-Methods', 'POST, GET, PUT, PATCH, DELETE, OPTIONS')
self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
def options(self, *args, **kwargs):
self.finish(self._no_content())
def json_response(self, data=None, error=None, status=200):
self.set_header('Content-Type', 'application/json')
def to_json(self, response):
return json.dumps(response)
self.set_status(status)
if error is not None:
return json.dumps({'error': error})
if data is not None:
return json.dumps(data)
return None
def _no_content(self):
return self.json_response(status=204)
def _unauthorized(self, error):
return self.json_response(error=error, status=401)
def _bad_request(self, error):
return self.json_response(error=error, status=400)
def _not_found(self, error):
return self.json_response(error=error, status=404)
def _validate_schema(self, schema, arguments):
return schema().validate({k: to_basestring(v[0]) if len(v) <= 1 else to_basestring(v) for k, v in arguments.items()})
......@@ -193,15 +224,23 @@ class APIBaseHandler(BaseHandler):
return spec.to_dict()
def get_argument(self, *args, **kwargs):
value = super(APIBaseHandler, self).get_argument(*args, **kwargs)
try:
return bleach.clean(value)
except TypeError:
return value
class ApiProfileHandler(APIBaseHandler):
def get(self):
return self.to_json(self.current_user)
return self.json_response(self.current_user)
class ApiPingHandler(APIBaseHandler):
def get(self):
return self.to_json({'message': 'pong'})
return self.json_response({'message': 'pong'})
class ApiSwaggerDotJsonHandler(APIBaseHandler):
......@@ -212,4 +251,4 @@ class ApiSwaggerDotJsonHandler(APIBaseHandler):
def get(self):
""" Get swagger.json """
return self.to_json(self.generate_swagger_json(self.api_handlers, self.api_version))
return self.json_response(self.generate_swagger_json(self.api_handlers, self.api_version))
......@@ -25,7 +25,6 @@ import os
import re
import time
import traceback
import types
from concurrent.futures.thread import ThreadPoolExecutor
from urllib.parse import unquote_plus
......
......@@ -19,7 +19,6 @@
# along with SiCKRAGE. If not, see <http://www.gnu.org/licenses/>.
# ##############################################################################
import os
from concurrent.futures.thread import ThreadPoolExecutor
import sickrage
from sickrage.core.webserver.handlers.api import APIBaseHandler
......@@ -27,15 +26,14 @@ from sickrage.core.webserver.handlers.api import APIBaseHandler
class ApiV2BaseHandler(APIBaseHandler):
def __init__(self, application, request, **kwargs):
super(ApiV2BaseHandler, self).__init__(application, request, **kwargs)
self.executor = ThreadPoolExecutor(thread_name_prefix='APIv2-Thread')
super(ApiV2BaseHandler, self).__init__(application, request, api_version='v2', **kwargs)
class ApiV2RetrieveSeriesMetadataHandler(ApiV2BaseHandler):
def get(self):
series_directory = self.get_argument('seriesDirectory', None)
if not series_directory:
return self.send_error(400, error="Missing seriesDirectory parameter")
return self._bad_request(error="Missing seriesDirectory parameter")
json_data = {
'rootDirectory': os.path.dirname(series_directory),
......@@ -61,4 +59,4 @@ class ApiV2RetrieveSeriesMetadataHandler(ApiV2BaseHandler):
if not json_data['seriesSlug'] and series_id and series_provider_id:
json_data['seriesSlug'] = f'{series_id}-{series_provider_id.slug}'
return self.to_json(json_data)
return self.json_response(json_data)
......@@ -59,4 +59,4 @@ class ApiV2ConfigHandler(ApiV2BaseHandler):
}
}
return self.to_json(config_data)
return self.json_response(config_data)
......@@ -28,7 +28,7 @@ class ApiV2FileBrowserHandler(ApiV2BaseHandler):
path = self.get_argument('path', None)
include_files = self.get_argument('includeFiles', None)
return self.to_json(self.get_path(path, bool(include_files)))
return self.json_response(self.get_path(path, bool(include_files)))
def get_path(self, path, include_files=False):
entries = {
......
......@@ -88,4 +88,4 @@ class ApiV2HistoryHandler(ApiV2BaseHandler):
results.append(row)
return self.to_json(results)
return self.json_response(results)
......@@ -70,15 +70,15 @@ class Apiv2PostProcessHandler(ApiV2BaseHandler):
validation_errors = self._validate_schema(PostProcessSchema, self.request.arguments)
if validation_errors:
return self.send_error(400, errors=validation_errors)
return self._bad_request(error=validation_errors)
if not path and not sickrage.app.config.general.tv_download_dir:
return self.send_error(400, error={"path": "You need to provide a path or set TV Download Dir"})
return self._bad_request(error={"path": "You need to provide a path or set TV Download Dir"})
json_data = sickrage.app.postprocessor_queue.put(path, nzbName=nzb_name, process_method=ProcessMethod[process_method.upper()], force=force_replace,
is_priority=is_priority, delete_on=delete, failed=failed, proc_type=proc_type, force_next=force_next)
if 'Processing succeeded' not in json_data:
return self.send_error(400, error=json_data)
if 'Processing succeeded' not in json_data and 'Successfully processed' not in json_data:
return self._bad_request(error=json_data)
return self.to_json({'data': json_data if return_data else ''})
return self.json_response({'data': json_data if return_data else ''})
......@@ -74,4 +74,4 @@ class ApiV2ScheduleHandler(ApiV2BaseHandler):
results[i]['localtime'] = result['localtime'].timestamp()
results[i] = convert_dict_keys_to_camelcase(results[i])
return self.to_json({'episodes': results, 'today': today.timestamp(), 'nextWeek': next_week.timestamp()})
return self.json_response({'episodes': results, 'today': today.timestamp(), 'nextWeek': next_week.timestamp()})
......@@ -87,13 +87,13 @@ class ApiV2SeriesHandler(ApiV2BaseHandler):
all_series.append(show.to_json(progress=True))
return self.to_json(all_series)
return self.json_response(all_series)
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
return self.to_json(series.to_json(episodes=True, details=True))
return self.json_response(series.to_json(episodes=True, details=True))
def post(self):
data = json_decode(self.request.body)
......@@ -123,18 +123,18 @@ class ApiV2SeriesHandler(ApiV2BaseHandler):
add_show_year = self._parse_boolean(data.get('addShowYear', 'false'))
if not series_id:
return self.send_error(400, error=f"Missing seriesId parameter: {series_id}")
return self._bad_request(error=f"Missing seriesId parameter: {series_id}")
series_provider_id = SeriesProviderID.by_slug(series_provider_slug)
if not series_provider_id:
return self.send_error(404, error="Unable to identify a series provider using provided slug")
return self._not_found(error="Unable to identify a series provider using provided slug")
series = find_show(int(series_id), series_provider_id)
if series:
return self.send_error(400, error=f"Already exists series: {series_id}")
return self._bad_request(error=f"Already exists series: {series_id}")
if is_existing and not series_directory:
return self.send_error(400, error="Missing seriesDirectory parameter")
return self._bad_request(error="Missing seriesDirectory parameter")
if not is_existing:
series_directory = os.path.join(root_directory, sanitize_file_name(series_name))
......@@ -146,12 +146,12 @@ class ApiV2SeriesHandler(ApiV2BaseHandler):
if os.path.isdir(series_directory):
sickrage.app.alerts.error(_("Unable to add show"), _("Folder ") + series_directory + _(" exists already"))
return self.send_error(400, error=f"Show directory {series_directory} already exists!")
return self._bad_request(error=f"Show directory {series_directory} already exists!")
if not make_dir(series_directory):
sickrage.app.log.warning(f"Unable to create the folder {series_directory}, can't add the show")
sickrage.app.alerts.error(_("Unable to add show"), _(f"Unable to create the folder {series_directory}, can't add the show"))
return self.send_error(400, error=f"Unable to create the show folder {series_directory}, can't add the show")
return self._bad_request(error=f"Unable to create the show folder {series_directory}, can't add the show")
chmod_as_parent(series_directory)
......@@ -181,7 +181,7 @@ class ApiV2SeriesHandler(ApiV2BaseHandler):
sickrage.app.alerts.message(_('Adding Show'), _(f'Adding the specified show into {series_directory}'))
return self.to_json({'message': True})
return self.json_response({'message': True})
def patch(self, series_slug):
warnings, errors = [], []
......@@ -193,7 +193,7 @@ class ApiV2SeriesHandler(ApiV2BaseHandler):
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._bad_request(error=f"Unable to find the specified series using slug: {series_slug}")
# if we changed the language then kick off an update
if data.get('lang') is not None and data['lang'] != series.lang:
......@@ -307,80 +307,80 @@ class ApiV2SeriesHandler(ApiV2BaseHandler):
# commit changes to database
series.save()
return self.to_json(series.to_json(episodes=True, details=True))
return self.json_response(series.to_json(episodes=True, details=True))
def delete(self, series_slug):
data = json_decode(self.request.body)
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
sickrage.app.show_queue.remove_show(series.series_id, series.series_provider_id, checkbox_to_value(data.get('delete')))
return self.to_json({'message': True})
return self.json_response({'message': True})
class ApiV2SeriesEpisodesHandler(ApiV2BaseHandler):
def get(self, series_slug, *args, **kwargs):
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
episodes = []
for episode in series.episodes:
episodes.append(episode.to_json())
return self.to_json(episodes)
return self.json_response(episodes)
class ApiV2SeriesImagesHandler(ApiV2BaseHandler):
def get(self, series_slug, *args, **kwargs):
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
image = series_image(series.series_id, series.series_provider_id, SeriesImageType.POSTER_THUMB)
return self.to_json({'poster': image.url})
return self.json_response({'poster': image.url})
class ApiV2SeriesImdbInfoHandler(ApiV2BaseHandler):
def get(self, series_slug, *args, **kwargs):
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
with sickrage.app.main_db.session() as session:
imdb_info = session.query(MainDB.IMDbInfo).filter_by(imdb_id=series.imdb_id).one_or_none()
json_data = IMDbInfoSchema().dump(imdb_info)
return self.to_json(json_data)
return self.json_response(json_data)
class ApiV2SeriesBlacklistHandler(ApiV2BaseHandler):
def get(self, series_slug, *args, **kwargs):
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
with sickrage.app.main_db.session() as session:
blacklist = session.query(MainDB.Blacklist).filter_by(series_id=series.series_id, series_provider_id=series.series_provider_id).one_or_none()
json_data = BlacklistSchema().dump(blacklist)
return self.to_json(json_data)
return self.json_response(json_data)
class ApiV2SeriesWhitelistHandler(ApiV2BaseHandler):
def get(self, series_slug, *args, **kwargs):
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
with sickrage.app.main_db.session() as session:
whitelist = session.query(MainDB.Whitelist).filter_by(series_id=series.series_id, series_provider_id=series.series_provider_id).one_or_none()
json_data = WhitelistSchema().dump(whitelist)
return self.to_json(json_data)
return self.json_response(json_data)
class ApiV2SeriesRefreshHandler(ApiV2BaseHandler):
......@@ -389,12 +389,12 @@ class ApiV2SeriesRefreshHandler(ApiV2BaseHandler):
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
try:
sickrage.app.show_queue.refresh_show(series.series_id, series.series_provider_id, force=bool(force))
except CantUpdateShowException as e:
return self.send_error(400, error=_(f"Unable to refresh this show, error: {e}"))
return self._bad_request(error=_(f"Unable to refresh this show, error: {e}"))
class ApiV2SeriesUpdateHandler(ApiV2BaseHandler):
......@@ -403,12 +403,12 @@ class ApiV2SeriesUpdateHandler(ApiV2BaseHandler):
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
try:
sickrage.app.show_queue.update_show(series.series_id, series.series_provider_id, force=bool(force))
except CantUpdateShowException as e:
return self.send_error(400, error=_(f"Unable to update this show, error: {e}"))
return self._bad_request(error=_(f"Unable to update this show, error: {e}"))
class ApiV2SeriesEpisodesRenameHandler(ApiV2BaseHandler):
......@@ -443,16 +443,16 @@ class ApiV2SeriesEpisodesRenameHandler(ApiV2BaseHandler):
NotAuthorizedSchema
"""
if not series_slug:
return self.send_error(400, error="Missing series slug")
return self._bad_request(error="Missing series slug")
rename_data = []
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
if not os.path.isdir(series.location):
return self.send_error(400, error="Can't rename episodes when the show location does not exist")
return self._bad_request(error="Can't rename episodes when the show location does not exist")
for episode in series.episodes:
if not episode.location:
......@@ -470,7 +470,7 @@ class ApiV2SeriesEpisodesRenameHandler(ApiV2BaseHandler):
'newLocation': new_location,
})
return self.to_json(rename_data)
return self.json_response(rename_data)
def post(self, series_slug):
"""Rename list of episodes"
......@@ -508,10 +508,10 @@ class ApiV2SeriesEpisodesRenameHandler(ApiV2BaseHandler):
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
if not os.path.isdir(series.location):
return self.send_error(400, error="Can't rename episodes when the show location does not exist")
return self._bad_request(error="Can't rename episodes when the show location does not exist")
for episode_id in data.get('episodeIdList', []):
episode = find_episode(episode_id, series.series_provider_id)
......@@ -522,7 +522,7 @@ class ApiV2SeriesEpisodesRenameHandler(ApiV2BaseHandler):
if len(renamed_episodes) > 0:
WebSocketMessage('SHOW_RENAMED', {'seriesSlug': series.slug}).push()
return self.to_json(renamed_episodes)
return self.json_response(renamed_episodes)
class ApiV2SeriesEpisodesManualSearchHandler(ApiV2BaseHandler):
......@@ -569,16 +569,16 @@ class ApiV2SeriesEpisodesManualSearchHandler(ApiV2BaseHandler):
# validation_errors = self._validate_schema(SeriesEpisodesManualSearchPath, self.request.path)
# if validation_errors:
# return self.send_error(400, errors=validation_errors)
# return self._bad_request(error=validation_errors)
#
# validation_errors = self._validate_schema(SeriesEpisodesManualSearchSchema, self.request.arguments)
# if validation_errors:
# return self.send_error(400, errors=validation_errors)
# return self._bad_request(error=validation_errors)
#
series = find_show_by_slug(series_slug)
if series is None:
return self.send_error(404, error=f"Unable to find the specified series using slug: {series_slug}")
return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
match = re.match(r'^s(?P<season>\d+)e(?P<episode>\d+)$', episode_slug)
season_num = match.group('season')
......@@ -586,7 +586,7 @@ class ApiV2SeriesEpisodesManualSearchHandler(ApiV2BaseHandler):
episode = series.get_episode(int(season_num), int(episode_num), no_create=True)
if episode is None:
return self.send_error(404, error=f"Unable to find the specified episode using slug: {episode_slug}")
return self._bad_request(error=f"Unable to find the specified episode using slug: {episode_slug}")
# make a queue item for it and put it on the queue
ep_queue_item = ManualSearchTask(int(episode.show.series_id),
......@@ -597,9 +597,6 @@ class ApiV2SeriesEpisodesManualSearchHandler(ApiV2BaseHandler):
sickrage.app.search_queue.put(ep_queue_item)
if not all([ep_queue_item.started, ep_queue_item.success]):
return self.to_json({'success': True})
return self.json_response({'success': True})
return self.send_error(
status_code=404,
error=_(f"Unable to find season {season_num} episode {episode_num} for show {series.name} on search providers")
)
return self._not_found(error=_(f"Unable to find season {season_num} episode {episode_num} for show {series.name} on search providers"))
......@@ -27,7 +27,7 @@ from sickrage.core.webserver.handlers.api.v2 import ApiV2BaseHandler
class ApiV2SeriesProvidersHandler(ApiV2BaseHandler):
def get(self):
return self.to_json([{'displayName': x.display_name, 'slug': x.slug} for x in SeriesProviderID])
return self.json_response([{'displayName': x.display_name, 'slug': x.slug} for x in SeriesProviderID])
class ApiV2SeriesProvidersSearchHandler(ApiV2BaseHandler):
......@@ -37,22 +37,22 @@ class ApiV2SeriesProvidersSearchHandler(ApiV2BaseHandler):
series_provider_id = SeriesProviderID.by_slug(series_provider_slug)
if not series_provider_id:
return self.send_error(400, reason="Unable to identify a series provider using provided slug")
return self._bad_request(error="Unable to identify a series provider using provided slug")
sickrage.app.log.debug(f"Searching for show with term: {search_term} on series provider: {sickrage.app.series_providers[series_provider_id].name}")
# search via series name
results = sickrage.app.series_providers[series_provider_id].search(search_term, language=lang)
if not results:
return self.send_error(404, reason=f"Unable to find the series using the search term: {search_term}")
return self._not_found(error=f"Unable to find the series using the search term: {search_term}")
return self.to_json(results)
return self.json_response(results)
class ApiV2SeriesProvidersLanguagesHandler(ApiV2BaseHandler):
def get(self, series_provider_slug):
series_provider_id = SeriesProviderID.by_slug(series_provider_slug)
if not series_provider_id:
return self.send_error(404, reason="Unable to identify a series provider using provided slug")
return self._not_found(error="Unable to identify a series provider using provided slug")
return self.to_json(sickrage.app.series_providers[series_provider_id].languages())
return self.json_response(sickrage.app.series_providers[series_provider_id].languages())
......@@ -6,7 +6,7 @@
%>
<%block name="metas">
<meta data-var="commands" data-content="${api_commands}">
## <meta data-var="commands" data-content="${api_commands}">
<meta data-var="episodes" data-content="${episodes}">
</%block>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment