diff --git a/bazarr/api/badges/badges.py b/bazarr/api/badges/badges.py index c660ed968..aa0a1ff08 100644 --- a/bazarr/api/badges/badges.py +++ b/bazarr/api/badges/badges.py @@ -4,7 +4,7 @@ import ast from functools import reduce -from flask_restx import Resource, Namespace, fields +from flask_restx import Resource, Namespace, fields, marshal from app.database import get_exclusion_clause, TableEpisodes, TableShows, TableMovies, database, select from app.get_providers import get_throttled_providers @@ -31,7 +31,6 @@ class Badges(Resource): }) @authenticate - @api_ns_badges.marshal_with(get_model, code=200) @api_ns_badges.response(401, 'Not Authenticated') @api_ns_badges.doc(parser=None) def get(self): @@ -74,4 +73,4 @@ def get(self): 'radarr_signalr': "LIVE" if radarr_signalr_client.connected else "", 'announcements': len(get_all_announcements()), } - return result + return marshal(result, self.get_model) diff --git a/bazarr/api/episodes/blacklist.py b/bazarr/api/episodes/blacklist.py index 7bcab005b..8c5ffc0c0 100644 --- a/bazarr/api/episodes/blacklist.py +++ b/bazarr/api/episodes/blacklist.py @@ -2,7 +2,7 @@ import pretty -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from app.database import TableEpisodes, TableShows, TableBlacklist, database, select from subtitles.tools.delete import delete_subtitles @@ -39,7 +39,6 @@ class EpisodesBlacklist(Resource): }) @authenticate - @api_ns_episodes_blacklist.marshal_with(get_response_model, envelope='data', code=200) @api_ns_episodes_blacklist.response(401, 'Not Authenticated') @api_ns_episodes_blacklist.doc(parser=get_request_parser) def get(self): @@ -63,7 +62,7 @@ def get(self): if length > 0: stmt = stmt.limit(length).offset(start) - return [postprocess({ + return marshal([postprocess({ 'seriesTitle': x.seriesTitle, 'episode_number': x.episode_number, 'episodeTitle': x.episodeTitle, @@ -73,7 +72,7 @@ def get(self): 'language': x.language, 'timestamp': pretty.date(x.timestamp), 'parsed_timestamp': x.timestamp.strftime('%x %X') - }) for x in database.execute(stmt).all()] + }) for x in database.execute(stmt).all()], self.get_response_model, envelope='data') post_request_parser = reqparse.RequestParser() post_request_parser.add_argument('seriesid', type=int, required=True, help='Series ID') @@ -88,7 +87,7 @@ def get(self): @api_ns_episodes_blacklist.response(200, 'Success') @api_ns_episodes_blacklist.response(401, 'Not Authenticated') @api_ns_episodes_blacklist.response(404, 'Episode not found') - @api_ns_episodes_blacklist.response(410, 'Subtitles file not found or permission issue.') + @api_ns_episodes_blacklist.response(500, 'Subtitles file not found or permission issue.') def post(self): """Add an episodes subtitles to blacklist""" args = self.post_request_parser.parse_args() @@ -126,7 +125,7 @@ def post(self): event_stream(type='episode-history') return '', 200 else: - return 'Subtitles file not found or permission issue.', 410 + return 'Subtitles file not found or permission issue.', 500 delete_request_parser = reqparse.RequestParser() delete_request_parser.add_argument('all', type=str, required=False, help='Empty episodes subtitles blacklist') diff --git a/bazarr/api/episodes/episodes.py b/bazarr/api/episodes/episodes.py index 00cdfea6e..e2d460c80 100644 --- a/bazarr/api/episodes/episodes.py +++ b/bazarr/api/episodes/episodes.py @@ -1,6 +1,6 @@ # coding=utf-8 -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from app.database import TableEpisodes, database, select from api.swaggerui import subtitles_model, subtitles_language_model, audio_language_model @@ -37,7 +37,6 @@ class Episodes(Resource): }) @authenticate - @api_ns_episodes.marshal_with(get_response_model, envelope='data', code=200) @api_ns_episodes.doc(parser=get_request_parser) @api_ns_episodes.response(200, 'Success') @api_ns_episodes.response(401, 'Not Authenticated') @@ -76,7 +75,7 @@ def get(self): else: return "Series or Episode ID not provided", 404 - return [postprocess({ + return marshal([postprocess({ 'audio_language': x.audio_language, 'episode': x.episode, 'missing_subtitles': x.missing_subtitles, @@ -88,4 +87,4 @@ def get(self): 'subtitles': x.subtitles, 'title': x.title, 'sceneName': x.sceneName, - }) for x in stmt_query] + }) for x in stmt_query], self.get_response_model, envelope='data') diff --git a/bazarr/api/episodes/episodes_subtitles.py b/bazarr/api/episodes/episodes_subtitles.py index 8af7e8da4..567bf7d41 100644 --- a/bazarr/api/episodes/episodes_subtitles.py +++ b/bazarr/api/episodes/episodes_subtitles.py @@ -38,7 +38,7 @@ class EpisodesSubtitles(Resource): @api_ns_episodes_subtitles.response(401, 'Not Authenticated') @api_ns_episodes_subtitles.response(404, 'Episode not found') @api_ns_episodes_subtitles.response(409, 'Unable to save subtitles file. Permission or path mapping issue?') - @api_ns_episodes_subtitles.response(410, 'Episode file not found. Path mapping issue?') + @api_ns_episodes_subtitles.response(500, 'Custom error messages') def patch(self): """Download an episode subtitles""" args = self.patch_request_parser.parse_args() @@ -60,7 +60,7 @@ def patch(self): episodePath = path_mappings.path_replace(episodeInfo.path) if not os.path.exists(episodePath): - return 'Episode file not found. Path mapping issue?', 410 + return 'Episode file not found. Path mapping issue?', 500 sceneName = episodeInfo.sceneName or "None" @@ -79,13 +79,14 @@ def patch(self): try: result = list(generate_subtitles(episodePath, [(language, hi, forced)], audio_language, sceneName, title, 'series', profile_id=get_profile_id(episode_id=sonarrEpisodeId))) - if result: + if isinstance(result, list) and len(result): result = result[0] history_log(1, sonarrSeriesId, sonarrEpisodeId, result) send_notifications(sonarrSeriesId, sonarrEpisodeId, result.message) store_subtitles(result.path, episodePath) else: event_stream(type='episode', payload=sonarrEpisodeId) + return 'No subtitles found', 500 except OSError: return 'Unable to save subtitles file. Permission or path mapping issue?', 409 else: @@ -106,7 +107,7 @@ def patch(self): @api_ns_episodes_subtitles.response(401, 'Not Authenticated') @api_ns_episodes_subtitles.response(404, 'Episode not found') @api_ns_episodes_subtitles.response(409, 'Unable to save subtitles file. Permission or path mapping issue?') - @api_ns_episodes_subtitles.response(410, 'Episode file not found. Path mapping issue?') + @api_ns_episodes_subtitles.response(500, 'Episode file not found. Path mapping issue?') def post(self): """Upload an episode subtitles""" args = self.post_request_parser.parse_args() @@ -124,7 +125,7 @@ def post(self): episodePath = path_mappings.path_replace(episodeInfo.path) if not os.path.exists(episodePath): - return 'Episode file not found. Path mapping issue?', 410 + return 'Episode file not found. Path mapping issue?', 500 audio_language = get_audio_profile_languages(episodeInfo.audio_language) if len(audio_language) and isinstance(audio_language[0], dict): @@ -178,7 +179,7 @@ def post(self): @api_ns_episodes_subtitles.response(204, 'Success') @api_ns_episodes_subtitles.response(401, 'Not Authenticated') @api_ns_episodes_subtitles.response(404, 'Episode not found') - @api_ns_episodes_subtitles.response(410, 'Subtitles file not found or permission issue.') + @api_ns_episodes_subtitles.response(500, 'Subtitles file not found or permission issue.') def delete(self): """Delete an episode subtitles""" args = self.delete_request_parser.parse_args() @@ -211,4 +212,4 @@ def delete(self): sonarr_episode_id=sonarrEpisodeId): return '', 204 else: - return 'Subtitles file not found or permission issue.', 410 + return 'Subtitles file not found or permission issue.', 500 diff --git a/bazarr/api/episodes/history.py b/bazarr/api/episodes/history.py index 606a19840..ca3e8a500 100644 --- a/bazarr/api/episodes/history.py +++ b/bazarr/api/episodes/history.py @@ -9,7 +9,7 @@ from subtitles.upgrade import get_upgradable_episode_subtitles, _language_still_desired import pretty -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from ..utils import authenticate, postprocess api_ns_episodes_history = Namespace('Episodes History', description='List episodes history events') @@ -53,7 +53,6 @@ class EpisodesHistory(Resource): }) @authenticate - @api_ns_episodes_history.marshal_with(get_response_model, code=200) @api_ns_episodes_history.response(401, 'Not Authenticated') @api_ns_episodes_history.doc(parser=get_request_parser) def get(self): @@ -176,4 +175,4 @@ def get(self): .where(TableEpisodes.title.is_not(None))) \ .scalar() - return {'data': episode_history, 'total': count} + return marshal({'data': episode_history, 'total': count}, self.get_response_model) diff --git a/bazarr/api/episodes/wanted.py b/bazarr/api/episodes/wanted.py index af6dd582b..ae7337751 100644 --- a/bazarr/api/episodes/wanted.py +++ b/bazarr/api/episodes/wanted.py @@ -2,7 +2,7 @@ import operator -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from functools import reduce from app.database import get_exclusion_clause, TableEpisodes, TableShows, database, select, func @@ -41,7 +41,6 @@ class EpisodesWanted(Resource): }) @authenticate - @api_ns_episodes_wanted.marshal_with(get_response_model, code=200) @api_ns_episodes_wanted.response(401, 'Not Authenticated') @api_ns_episodes_wanted.doc(parser=get_request_parser) def get(self): @@ -96,4 +95,4 @@ def get(self): .where(wanted_condition)) \ .scalar() - return {'data': results, 'total': count} + return marshal({'data': results, 'total': count}, self.get_response_model) diff --git a/bazarr/api/files/files.py b/bazarr/api/files/files.py index 434faeae9..fccb05ce6 100644 --- a/bazarr/api/files/files.py +++ b/bazarr/api/files/files.py @@ -1,6 +1,6 @@ # coding=utf-8 -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from utilities.filesystem import browse_bazarr_filesystem @@ -21,7 +21,6 @@ class BrowseBazarrFS(Resource): }) @authenticate - @api_ns_files.marshal_with(get_response_model, code=200) @api_ns_files.response(401, 'Not Authenticated') @api_ns_files.doc(parser=get_request_parser) def get(self): @@ -37,4 +36,4 @@ def get(self): return [] for item in result['directories']: data.append({'name': item['name'], 'children': True, 'path': item['path']}) - return data + return marshal(data, self.get_response_model) diff --git a/bazarr/api/files/files_radarr.py b/bazarr/api/files/files_radarr.py index b6d6eed33..2f4d7e802 100644 --- a/bazarr/api/files/files_radarr.py +++ b/bazarr/api/files/files_radarr.py @@ -1,6 +1,6 @@ # coding=utf-8 -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from radarr.filesystem import browse_radarr_filesystem @@ -22,7 +22,6 @@ class BrowseRadarrFS(Resource): }) @authenticate - @api_ns_files_radarr.marshal_with(get_response_model, code=200) @api_ns_files_radarr.response(401, 'Not Authenticated') @api_ns_files_radarr.doc(parser=get_request_parser) def get(self): @@ -38,4 +37,4 @@ def get(self): return [] for item in result['directories']: data.append({'name': item['name'], 'children': True, 'path': item['path']}) - return data + return marshal(data, self.get_response_model) diff --git a/bazarr/api/files/files_sonarr.py b/bazarr/api/files/files_sonarr.py index f82bab9b7..61dd2d587 100644 --- a/bazarr/api/files/files_sonarr.py +++ b/bazarr/api/files/files_sonarr.py @@ -1,6 +1,6 @@ # coding=utf-8 -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from sonarr.filesystem import browse_sonarr_filesystem @@ -22,7 +22,6 @@ class BrowseSonarrFS(Resource): }) @authenticate - @api_ns_files_sonarr.marshal_with(get_response_model, code=200) @api_ns_files_sonarr.response(401, 'Not Authenticated') @api_ns_files_sonarr.doc(parser=get_request_parser) def get(self): @@ -38,4 +37,4 @@ def get(self): return [] for item in result['directories']: data.append({'name': item['name'], 'children': True, 'path': item['path']}) - return data + return marshal(data, self.get_response_model) diff --git a/bazarr/api/history/stats.py b/bazarr/api/history/stats.py index 73c54cfb7..bc891f3ae 100644 --- a/bazarr/api/history/stats.py +++ b/bazarr/api/history/stats.py @@ -5,7 +5,7 @@ import itertools from dateutil import rrule -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from functools import reduce from app.database import TableHistory, TableHistoryMovie, database, select @@ -41,7 +41,6 @@ class HistoryStats(Resource): }) @authenticate - @api_ns_history_stats.marshal_with(get_response_model, code=200) @api_ns_history_stats.response(401, 'Not Authenticated') @api_ns_history_stats.doc(parser=get_request_parser) def get(self): @@ -121,4 +120,4 @@ def get(self): sorted_data_series = sorted(data_series, key=lambda i: i['date']) sorted_data_movies = sorted(data_movies, key=lambda i: i['date']) - return {'series': sorted_data_series, 'movies': sorted_data_movies} + return marshal({'series': sorted_data_series, 'movies': sorted_data_movies}, self.get_response_model) diff --git a/bazarr/api/movies/blacklist.py b/bazarr/api/movies/blacklist.py index 22bb09ce9..60c069597 100644 --- a/bazarr/api/movies/blacklist.py +++ b/bazarr/api/movies/blacklist.py @@ -2,7 +2,7 @@ import pretty -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from app.database import TableMovies, TableBlacklistMovie, database, select from subtitles.tools.delete import delete_subtitles @@ -37,7 +37,6 @@ class MoviesBlacklist(Resource): }) @authenticate - @api_ns_movies_blacklist.marshal_with(get_response_model, envelope='data', code=200) @api_ns_movies_blacklist.response(401, 'Not Authenticated') @api_ns_movies_blacklist.doc(parser=get_request_parser) def get(self): @@ -59,7 +58,7 @@ def get(self): if length > 0: data = data.limit(length).offset(start) - return [postprocess({ + return marshal([postprocess({ 'title': x.title, 'radarrId': x.radarrId, 'provider': x.provider, @@ -67,7 +66,7 @@ def get(self): 'language': x.language, 'timestamp': pretty.date(x.timestamp), 'parsed_timestamp': x.timestamp.strftime('%x %X'), - }) for x in data.all()] + }) for x in data.all()], self.get_response_model, envelope='data') post_request_parser = reqparse.RequestParser() post_request_parser.add_argument('radarrid', type=int, required=True, help='Radarr ID') @@ -81,7 +80,7 @@ def get(self): @api_ns_movies_blacklist.response(200, 'Success') @api_ns_movies_blacklist.response(401, 'Not Authenticated') @api_ns_movies_blacklist.response(404, 'Movie not found') - @api_ns_movies_blacklist.response(410, 'Subtitles file not found or permission issue.') + @api_ns_movies_blacklist.response(500, 'Subtitles file not found or permission issue.') def post(self): """Add a movies subtitles to blacklist""" args = self.post_request_parser.parse_args() @@ -119,7 +118,7 @@ def post(self): event_stream(type='movie-history') return '', 200 else: - return 'Subtitles file not found or permission issue.', 410 + return 'Subtitles file not found or permission issue.', 500 delete_request_parser = reqparse.RequestParser() delete_request_parser.add_argument('all', type=str, required=False, help='Empty movies subtitles blacklist') diff --git a/bazarr/api/movies/history.py b/bazarr/api/movies/history.py index 0469439b4..d7587607c 100644 --- a/bazarr/api/movies/history.py +++ b/bazarr/api/movies/history.py @@ -4,7 +4,7 @@ import pretty import ast -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from functools import reduce from app.database import TableMovies, TableHistoryMovie, TableBlacklistMovie, database, select, func @@ -52,7 +52,6 @@ class MoviesHistory(Resource): }) @authenticate - @api_ns_movies_history.marshal_with(get_response_model, code=200) @api_ns_movies_history.response(401, 'Not Authenticated') @api_ns_movies_history.doc(parser=get_request_parser) def get(self): @@ -167,4 +166,4 @@ def get(self): .where(TableMovies.title.is_not(None))) \ .scalar() - return {'data': movie_history, 'total': count} + return marshal({'data': movie_history, 'total': count}, self.get_response_model) diff --git a/bazarr/api/movies/movies.py b/bazarr/api/movies/movies.py index ec181b2f8..022769d9d 100644 --- a/bazarr/api/movies/movies.py +++ b/bazarr/api/movies/movies.py @@ -1,6 +1,6 @@ # coding=utf-8 -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from app.database import TableMovies, database, update, select, func from subtitles.indexer.movies import list_missing_subtitles_movies, movies_scan_subtitles @@ -52,7 +52,6 @@ class Movies(Resource): }) @authenticate - @api_ns_movies.marshal_with(get_response_model, code=200) @api_ns_movies.doc(parser=get_request_parser) @api_ns_movies.response(200, 'Success') @api_ns_movies.response(401, 'Not Authenticated') @@ -112,7 +111,7 @@ def get(self): .select_from(TableMovies)) \ .scalar() - return {'data': results, 'total': count} + return marshal({'data': results, 'total': count}, self.get_response_model) post_request_parser = reqparse.RequestParser() post_request_parser.add_argument('radarrid', type=int, action='append', required=False, default=[], @@ -166,7 +165,7 @@ def post(self): @api_ns_movies.response(204, 'Success') @api_ns_movies.response(400, 'Unknown action') @api_ns_movies.response(401, 'Not Authenticated') - @api_ns_movies.response(410, 'Movie file not found. Path mapping issue?') + @api_ns_movies.response(500, 'Movie file not found. Path mapping issue?') def patch(self): """Run actions on specific movies""" args = self.patch_request_parser.parse_args() @@ -179,7 +178,7 @@ def patch(self): try: movies_download_subtitles(radarrid) except OSError: - return 'Movie file not found. Path mapping issue?', 410 + return 'Movie file not found. Path mapping issue?', 500 else: return '', 204 elif action == "search-wanted": diff --git a/bazarr/api/movies/movies_subtitles.py b/bazarr/api/movies/movies_subtitles.py index f6a31180d..9d564bcb5 100644 --- a/bazarr/api/movies/movies_subtitles.py +++ b/bazarr/api/movies/movies_subtitles.py @@ -37,7 +37,7 @@ class MoviesSubtitles(Resource): @api_ns_movies_subtitles.response(401, 'Not Authenticated') @api_ns_movies_subtitles.response(404, 'Movie not found') @api_ns_movies_subtitles.response(409, 'Unable to save subtitles file. Permission or path mapping issue?') - @api_ns_movies_subtitles.response(410, 'Movie file not found. Path mapping issue?') + @api_ns_movies_subtitles.response(500, 'Custom error messages') def patch(self): """Download a movie subtitles""" args = self.patch_request_parser.parse_args() @@ -58,7 +58,7 @@ def patch(self): moviePath = path_mappings.path_replace_movie(movieInfo.path) if not os.path.exists(moviePath): - return 'Movie file not found. Path mapping issue?', 410 + return 'Movie file not found. Path mapping issue?', 500 sceneName = movieInfo.sceneName or 'None' @@ -77,12 +77,13 @@ def patch(self): try: result = list(generate_subtitles(moviePath, [(language, hi, forced)], audio_language, sceneName, title, 'movie', profile_id=get_profile_id(movie_id=radarrId))) - if result: + if isinstance(result, list) and len(result): result = result[0] history_log_movie(1, radarrId, result) store_subtitles_movie(result.path, moviePath) else: event_stream(type='movie', payload=radarrId) + return 'No subtitles found', 500 except OSError: return 'Unable to save subtitles file. Permission or path mapping issue?', 409 else: @@ -103,7 +104,7 @@ def patch(self): @api_ns_movies_subtitles.response(401, 'Not Authenticated') @api_ns_movies_subtitles.response(404, 'Movie not found') @api_ns_movies_subtitles.response(409, 'Unable to save subtitles file. Permission or path mapping issue?') - @api_ns_movies_subtitles.response(410, 'Movie file not found. Path mapping issue?') + @api_ns_movies_subtitles.response(500, 'Movie file not found. Path mapping issue?') def post(self): """Upload a movie subtitles""" # TODO: Support Multiply Upload @@ -120,7 +121,7 @@ def post(self): moviePath = path_mappings.path_replace_movie(movieInfo.path) if not os.path.exists(moviePath): - return 'Movie file not found. Path mapping issue?', 410 + return 'Movie file not found. Path mapping issue?', 500 audio_language = get_audio_profile_languages(movieInfo.audio_language) if len(audio_language) and isinstance(audio_language[0], dict): @@ -174,7 +175,7 @@ def post(self): @api_ns_movies_subtitles.response(204, 'Success') @api_ns_movies_subtitles.response(401, 'Not Authenticated') @api_ns_movies_subtitles.response(404, 'Movie not found') - @api_ns_movies_subtitles.response(410, 'Subtitles file not found or permission issue.') + @api_ns_movies_subtitles.response(500, 'Subtitles file not found or permission issue.') def delete(self): """Delete a movie subtitles""" args = self.delete_request_parser.parse_args() @@ -205,4 +206,4 @@ def delete(self): radarr_id=radarrId): return '', 204 else: - return 'Subtitles file not found or permission issue.', 410 + return 'Subtitles file not found or permission issue.', 500 diff --git a/bazarr/api/movies/wanted.py b/bazarr/api/movies/wanted.py index 256788954..7ee648fc5 100644 --- a/bazarr/api/movies/wanted.py +++ b/bazarr/api/movies/wanted.py @@ -2,7 +2,7 @@ import operator -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from functools import reduce from app.database import get_exclusion_clause, TableMovies, database, select, func @@ -38,7 +38,6 @@ class MoviesWanted(Resource): }) @authenticate - @api_ns_movies_wanted.marshal_with(get_response_model, code=200) @api_ns_movies_wanted.response(401, 'Not Authenticated') @api_ns_movies_wanted.doc(parser=get_request_parser) def get(self): @@ -81,4 +80,4 @@ def get(self): .where(wanted_condition)) \ .scalar() - return {'data': results, 'total': count} + return marshal({'data': results, 'total': count}, self.get_response_model) diff --git a/bazarr/api/providers/providers.py b/bazarr/api/providers/providers.py index 41f054fea..eb74987d7 100644 --- a/bazarr/api/providers/providers.py +++ b/bazarr/api/providers/providers.py @@ -1,6 +1,6 @@ # coding=utf-8 -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from operator import itemgetter from app.database import TableHistory, TableHistoryMovie, database, select @@ -23,7 +23,6 @@ class Providers(Resource): }) @authenticate - @api_ns_providers.marshal_with(get_response_model, envelope='data', code=200) @api_ns_providers.response(200, 'Success') @api_ns_providers.response(401, 'Not Authenticated') @api_ns_providers.doc(parser=get_request_parser) @@ -61,7 +60,7 @@ def get(self): "status": provider[1] if provider[1] is not None else "Good", "retry": provider[2] if provider[2] != "now" else "-" }) - return sorted(providers_dicts, key=itemgetter('name')) + return marshal(sorted(providers_dicts, key=itemgetter('name')), self.get_response_model, envelope='data') post_request_parser = reqparse.RequestParser() post_request_parser.add_argument('action', type=str, required=True, help='Action to perform from ["reset"]') diff --git a/bazarr/api/providers/providers_episodes.py b/bazarr/api/providers/providers_episodes.py index 4e4e5a41c..48dd5c358 100644 --- a/bazarr/api/providers/providers_episodes.py +++ b/bazarr/api/providers/providers_episodes.py @@ -2,7 +2,7 @@ import os -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from app.database import TableEpisodes, TableShows, get_audio_profile_languages, get_profile_id, database, select from utilities.path_mappings import path_mappings @@ -12,6 +12,7 @@ from app.config import settings from app.notifier import send_notifications from subtitles.indexer.series import store_subtitles +from subtitles.processing import ProcessSubtitlesResult from ..utils import authenticate @@ -41,10 +42,9 @@ class ProviderEpisodes(Resource): }) @authenticate - @api_ns_providers_episodes.marshal_with(get_response_model, envelope='data', code=200) @api_ns_providers_episodes.response(401, 'Not Authenticated') @api_ns_providers_episodes.response(404, 'Episode not found') - @api_ns_providers_episodes.response(410, 'Episode file not found. Path mapping issue?') + @api_ns_providers_episodes.response(500, 'Custom error messages') @api_ns_providers_episodes.doc(parser=get_request_parser) def get(self): """Search manually for an episode subtitles""" @@ -67,7 +67,7 @@ def get(self): episodePath = path_mappings.path_replace(episodeInfo.path) if not os.path.exists(episodePath): - return 'Episode file not found. Path mapping issue?', 410 + return 'Episode file not found. Path mapping issue?', 500 sceneName = episodeInfo.sceneName or "None" profileId = episodeInfo.profileId @@ -75,9 +75,9 @@ def get(self): providers_list = get_providers() data = manual_search(episodePath, profileId, providers_list, sceneName, title, 'series') - if not data: - data = [] - return data + if isinstance(data, str): + return data, 500 + return marshal(data, self.get_response_model, envelope='data') post_request_parser = reqparse.RequestParser() post_request_parser.add_argument('seriesid', type=int, required=True, help='Series ID') @@ -94,6 +94,7 @@ def get(self): @api_ns_providers_episodes.response(204, 'Success') @api_ns_providers_episodes.response(401, 'Not Authenticated') @api_ns_providers_episodes.response(404, 'Episode not found') + @api_ns_providers_episodes.response(500, 'Custom error messages') def post(self): """Manually download an episode subtitles""" args = self.post_request_parser.parse_args() @@ -133,12 +134,15 @@ def post(self): result = manual_download_subtitle(episodePath, audio_language, hi, forced, subtitle, selected_provider, sceneName, title, 'series', use_original_format, profile_id=get_profile_id(episode_id=sonarrEpisodeId)) - if result: + except OSError: + return 'Unable to save subtitles file', 500 + else: + if isinstance(result, ProcessSubtitlesResult): history_log(2, sonarrSeriesId, sonarrEpisodeId, result) if not settings.general.dont_notify_manual_actions: send_notifications(sonarrSeriesId, sonarrEpisodeId, result.message) store_subtitles(result.path, episodePath) - except OSError: - pass - - return '', 204 + elif isinstance(result, str): + return result, 500 + else: + return '', 204 diff --git a/bazarr/api/providers/providers_movies.py b/bazarr/api/providers/providers_movies.py index 8080ea5cb..1a8c75760 100644 --- a/bazarr/api/providers/providers_movies.py +++ b/bazarr/api/providers/providers_movies.py @@ -2,7 +2,7 @@ import os -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from app.database import TableMovies, get_audio_profile_languages, get_profile_id, database, select from utilities.path_mappings import path_mappings @@ -12,6 +12,7 @@ from app.config import settings from app.notifier import send_notifications_movie from subtitles.indexer.movies import store_subtitles_movie +from subtitles.processing import ProcessSubtitlesResult from ..utils import authenticate @@ -42,10 +43,9 @@ class ProviderMovies(Resource): }) @authenticate - @api_ns_providers_movies.marshal_with(get_response_model, envelope='data', code=200) @api_ns_providers_movies.response(401, 'Not Authenticated') @api_ns_providers_movies.response(404, 'Movie not found') - @api_ns_providers_movies.response(410, 'Movie file not found. Path mapping issue?') + @api_ns_providers_movies.response(500, 'Custom error messages') @api_ns_providers_movies.doc(parser=get_request_parser) def get(self): """Search manually for a movie subtitles""" @@ -66,7 +66,7 @@ def get(self): moviePath = path_mappings.path_replace_movie(movieInfo.path) if not os.path.exists(moviePath): - return 'Movie file not found. Path mapping issue?', 410 + return 'Movie file not found. Path mapping issue?', 500 sceneName = movieInfo.sceneName or "None" profileId = movieInfo.profileId @@ -74,9 +74,9 @@ def get(self): providers_list = get_providers() data = manual_search(moviePath, profileId, providers_list, sceneName, title, 'movie') - if not data: - data = [] - return data + if isinstance(data, str): + return data, 500 + return marshal(data, self.get_response_model, envelope='data') post_request_parser = reqparse.RequestParser() post_request_parser.add_argument('radarrid', type=int, required=True, help='Movie ID') @@ -92,6 +92,7 @@ def get(self): @api_ns_providers_movies.response(204, 'Success') @api_ns_providers_movies.response(401, 'Not Authenticated') @api_ns_providers_movies.response(404, 'Movie not found') + @api_ns_providers_movies.response(500, 'Custom error messages') def post(self): """Manually download a movie subtitles""" args = self.post_request_parser.parse_args() @@ -127,12 +128,15 @@ def post(self): result = manual_download_subtitle(moviePath, audio_language, hi, forced, subtitle, selected_provider, sceneName, title, 'movie', use_original_format, profile_id=get_profile_id(movie_id=radarrId)) - if result is not None: + except OSError: + return 'Unable to save subtitles file', 500 + else: + if isinstance(result, ProcessSubtitlesResult): history_log_movie(2, radarrId, result) if not settings.general.dont_notify_manual_actions: send_notifications_movie(radarrId, result.message) store_subtitles_movie(result.path, moviePath) - except OSError: - pass - - return '', 204 + elif isinstance(result, str): + return result, 500 + else: + return '', 204 diff --git a/bazarr/api/series/series.py b/bazarr/api/series/series.py index 0d6d1e8de..13bda6f0d 100644 --- a/bazarr/api/series/series.py +++ b/bazarr/api/series/series.py @@ -2,7 +2,7 @@ import operator -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from functools import reduce from app.database import get_exclusion_clause, TableEpisodes, TableShows, database, select, update, func @@ -56,7 +56,6 @@ class Series(Resource): }) @authenticate - @api_ns_series.marshal_with(get_response_model, code=200) @api_ns_series.doc(parser=get_request_parser) @api_ns_series.response(200, 'Success') @api_ns_series.response(401, 'Not Authenticated') @@ -137,7 +136,7 @@ def get(self): .select_from(TableShows)) \ .scalar() - return {'data': results, 'total': count} + return marshal({'data': results, 'total': count}, self.get_response_model) post_request_parser = reqparse.RequestParser() post_request_parser.add_argument('seriesid', type=int, action='append', required=False, default=[], @@ -199,7 +198,7 @@ def post(self): @api_ns_series.response(204, 'Success') @api_ns_series.response(400, 'Unknown action') @api_ns_series.response(401, 'Not Authenticated') - @api_ns_series.response(410, 'Series directory not found. Path mapping issue?') + @api_ns_series.response(500, 'Series directory not found. Path mapping issue?') def patch(self): """Run actions on specific series""" args = self.patch_request_parser.parse_args() @@ -212,7 +211,7 @@ def patch(self): try: series_download_subtitles(seriesid) except OSError: - return 'Series directory not found. Path mapping issue?', 410 + return 'Series directory not found. Path mapping issue?', 500 else: return '', 204 elif action == "search-wanted": diff --git a/bazarr/api/subtitles/subtitles.py b/bazarr/api/subtitles/subtitles.py index afd3cdb94..eb021613e 100644 --- a/bazarr/api/subtitles/subtitles.py +++ b/bazarr/api/subtitles/subtitles.py @@ -43,7 +43,7 @@ class Subtitles(Resource): @api_ns_subtitles.response(401, 'Not Authenticated') @api_ns_subtitles.response(404, 'Episode/movie not found') @api_ns_subtitles.response(409, 'Unable to edit subtitles file. Check logs.') - @api_ns_subtitles.response(410, 'Subtitles file not found. Path mapping issue?') + @api_ns_subtitles.response(500, 'Subtitles file not found. Path mapping issue?') def patch(self): """Apply mods/tools on external subtitles""" args = self.patch_request_parser.parse_args() @@ -55,7 +55,7 @@ def patch(self): id = args.get('id') if not os.path.exists(subtitles_path): - return 'Subtitles file not found. Path mapping issue?', 410 + return 'Subtitles file not found. Path mapping issue?', 500 if media_type == 'episode': metadata = database.execute( diff --git a/bazarr/api/subtitles/subtitles_info.py b/bazarr/api/subtitles/subtitles_info.py index 405a44cae..a154be24f 100644 --- a/bazarr/api/subtitles/subtitles_info.py +++ b/bazarr/api/subtitles/subtitles_info.py @@ -1,6 +1,6 @@ # coding=utf-8 -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from subliminal_patch.core import guessit from ..utils import authenticate @@ -24,7 +24,6 @@ class SubtitleNameInfo(Resource): }) @authenticate - @api_ns_subtitles_info.marshal_with(get_response_model, envelope='data', code=200) @api_ns_subtitles_info.response(200, 'Success') @api_ns_subtitles_info.response(401, 'Not Authenticated') @api_ns_subtitles_info.doc(parser=get_request_parser) @@ -60,4 +59,4 @@ def get(self): results.append(result) - return results + return marshal(results, self.get_response_model, envelope='data') diff --git a/bazarr/api/system/backups.py b/bazarr/api/system/backups.py index a2ee5c2f4..bbb499800 100644 --- a/bazarr/api/system/backups.py +++ b/bazarr/api/system/backups.py @@ -1,6 +1,6 @@ # coding=utf-8 -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from utilities.backup import get_backup_files, prepare_restore, delete_backup_file, backup_to_zip @@ -19,14 +19,13 @@ class SystemBackups(Resource): }) @authenticate - @api_ns_system_backups.marshal_with(get_response_model, envelope='data', code=200) @api_ns_system_backups.doc(parser=None) @api_ns_system_backups.response(204, 'Success') @api_ns_system_backups.response(401, 'Not Authenticated') def get(self): """List backup files""" backups = get_backup_files(fullpath=False) - return backups + return marshal(backups, self.get_response_model, envelope='data') @authenticate @api_ns_system_backups.doc(parser=None) diff --git a/bazarr/api/system/logs.py b/bazarr/api/system/logs.py index 501ff65d0..606b900d5 100644 --- a/bazarr/api/system/logs.py +++ b/bazarr/api/system/logs.py @@ -3,7 +3,7 @@ import io import os -from flask_restx import Resource, Namespace, fields +from flask_restx import Resource, Namespace, fields, marshal from app.logger import empty_log from app.get_args import args @@ -23,7 +23,6 @@ class SystemLogs(Resource): }) @authenticate - @api_ns_system_logs.marshal_with(get_response_model, envelope='data', code=200) @api_ns_system_logs.doc(parser=None) @api_ns_system_logs.response(200, 'Success') @api_ns_system_logs.response(401, 'Not Authenticated') @@ -50,7 +49,7 @@ def get(self): logs.append(log) logs.reverse() - return logs + return marshal(logs, self.get_response_model, envelope='data') @authenticate @api_ns_system_logs.doc(parser=None) diff --git a/bazarr/api/system/releases.py b/bazarr/api/system/releases.py index 7abd05ee7..e4848c597 100644 --- a/bazarr/api/system/releases.py +++ b/bazarr/api/system/releases.py @@ -5,7 +5,7 @@ import os import logging -from flask_restx import Resource, Namespace, fields +from flask_restx import Resource, Namespace, fields, marshal from app.config import settings from app.get_args import args @@ -26,7 +26,6 @@ class SystemReleases(Resource): }) @authenticate - @api_ns_system_releases.marshal_with(get_response_model, envelope='data', code=200) @api_ns_system_releases.doc(parser=None) @api_ns_system_releases.response(200, 'Success') @api_ns_system_releases.response(401, 'Not Authenticated') @@ -60,4 +59,4 @@ def get(self): except Exception: logging.exception( 'BAZARR cannot parse releases caching file: ' + os.path.join(args.config_dir, 'config', 'releases.txt')) - return filtered_releases + return marshal(filtered_releases, self.get_response_model, envelope='data') diff --git a/bazarr/api/system/tasks.py b/bazarr/api/system/tasks.py index 21d513d4b..264427735 100644 --- a/bazarr/api/system/tasks.py +++ b/bazarr/api/system/tasks.py @@ -1,6 +1,6 @@ # coding=utf-8 -from flask_restx import Resource, Namespace, reqparse, fields +from flask_restx import Resource, Namespace, reqparse, fields, marshal from app.scheduler import scheduler @@ -24,7 +24,6 @@ class SystemTasks(Resource): get_request_parser.add_argument('taskid', type=str, required=False, help='List tasks or a single task properties') @authenticate - @api_ns_system_tasks.marshal_with(get_response_model, envelope='data', code=200) @api_ns_system_tasks.doc(parser=None) @api_ns_system_tasks.response(200, 'Success') @api_ns_system_tasks.response(401, 'Not Authenticated') @@ -41,7 +40,7 @@ def get(self): task_list = [item] continue - return task_list + return marshal(task_list, self.get_response_model, envelope='data') post_request_parser = reqparse.RequestParser() post_request_parser.add_argument('taskid', type=str, required=True, help='Task id of the task to run') diff --git a/bazarr/app/config.py b/bazarr/app/config.py index f68004101..df1441309 100644 --- a/bazarr/app/config.py +++ b/bazarr/app/config.py @@ -87,7 +87,7 @@ class Validator(OriginalValidator): Validator('general.ignore_pgs_subs', must_exist=True, default=False, is_type_of=bool), Validator('general.ignore_vobsub_subs', must_exist=True, default=False, is_type_of=bool), Validator('general.ignore_ass_subs', must_exist=True, default=False, is_type_of=bool), - Validator('general.adaptive_searching', must_exist=True, default=False, is_type_of=bool), + Validator('general.adaptive_searching', must_exist=True, default=True, is_type_of=bool), Validator('general.adaptive_searching_delay', must_exist=True, default='3w', is_type_of=str, is_in=['1w', '2w', '3w', '4w']), Validator('general.adaptive_searching_delta', must_exist=True, default='1w', is_type_of=str, diff --git a/bazarr/app/get_providers.py b/bazarr/app/get_providers.py index 466517c62..7f619d23c 100644 --- a/bazarr/app/get_providers.py +++ b/bazarr/app/get_providers.py @@ -24,6 +24,7 @@ from utilities.binaries import get_binary from radarr.blacklist import blacklist_log_movie from sonarr.blacklist import blacklist_log +from utilities.analytics import event_tracker def time_until_midnight(timezone): @@ -337,6 +338,8 @@ def provider_throttle(name, exception): logging.info("Throttling %s for %s, until %s, because of: %s. Exception info: %r", name, throttle_description, throttle_until.strftime("%y/%m/%d %H:%M"), cls_name, exception.args[0] if exception.args else None) + event_tracker.track_throttling(provider=name, exception_name=cls_name, exception_info=exception.args[0] + if exception.args else None) update_throttled_provider() diff --git a/bazarr/subtitles/manual.py b/bazarr/subtitles/manual.py index 00bf87134..ca454e7ee 100644 --- a/bazarr/subtitles/manual.py +++ b/bazarr/subtitles/manual.py @@ -41,7 +41,7 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type): video = get_video(force_unicode(path), title, sceneName, providers=providers, media_type=media_type) else: logging.info("BAZARR All providers are throttled") - return None + return 'All providers are throttled' if video: try: if providers: @@ -62,7 +62,7 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type): else: subtitles = [] logging.info("BAZARR All providers are throttled") - return None + return 'All providers are throttled' except Exception: logging.exception("BAZARR Error trying to get Subtitle list from provider for this file: " + path) else: @@ -183,14 +183,14 @@ def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provide logging.debug('BAZARR Subtitles file downloaded for this file:' + path) else: logging.info("BAZARR All providers are throttled") - return None + return 'All providers are throttled' except Exception: logging.exception('BAZARR Error downloading Subtitles for this file ' + path) - return None + return 'Error downloading Subtitles' else: if not subtitle.is_valid(): logging.exception('BAZARR No valid Subtitles file found for this file: ' + path) - return + return 'No valid Subtitles file found' try: chmod = int(settings.general.chmod, 8) if not sys.platform.startswith( 'win') and settings.general.chmod_enabled else None @@ -203,7 +203,7 @@ def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provide path_decoder=force_unicode) except Exception: logging.exception('BAZARR Error saving Subtitles file to disk for this file:' + path) - return + return 'Error saving Subtitles file to disk' else: if saved_subtitles: _, max_score, _ = _get_scores(media_type) @@ -221,7 +221,7 @@ def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provide "BAZARR Tried to manually download a Subtitles for file: " + path + " but we weren't able to do (probably throttled by " + str(subtitle.provider_name) + ". Please retry later or select a Subtitles from another provider.") - return None + return 'Something went wrong, check the logs for error' subliminal.region.backend.sync() diff --git a/bazarr/subtitles/processing.py b/bazarr/subtitles/processing.py index 01d82d2a1..b924dcbaa 100644 --- a/bazarr/subtitles/processing.py +++ b/bazarr/subtitles/processing.py @@ -137,7 +137,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u notify_radarr(movie_metadata.radarrId) event_stream(type='movie-wanted', action='delete', payload=movie_metadata.radarrId) - event_tracker.track(provider=downloaded_provider, action=action, language=downloaded_language) + event_tracker.track_subtitles(provider=downloaded_provider, action=action, language=downloaded_language) return ProcessSubtitlesResult(message=message, reversed_path=reversed_path, @@ -148,7 +148,7 @@ def process_subtitle(subtitle, media_type, audio_language, path, max_score, is_u subtitle_id=subtitle.id, reversed_subtitles_path=reversed_subtitles_path, hearing_impaired=subtitle.language.hi, - matched=list(subtitle.matches), + matched=list(subtitle.matches or []), not_matched=_get_not_matched(subtitle, media_type)) diff --git a/bazarr/utilities/analytics.py b/bazarr/utilities/analytics.py index 7fe72a1ed..9d9e06138 100644 --- a/bazarr/utilities/analytics.py +++ b/bazarr/utilities/analytics.py @@ -47,8 +47,8 @@ def start_tracker(self): self.tracker.store.save() - def track(self, provider, action, language): - if not settings.analyticsenabled: + def track_subtitles(self, provider, action, language): + if not settings.analytics.enabled: return subtitles_event = self.tracker.create_new_event(name="subtitles") @@ -64,5 +64,22 @@ def track(self, provider, action, language): else: self.tracker.store.save() + def track_throttling(self, provider, exception_name, exception_info): + if not settings.analytics.getboolean('enabled'): + return + + throttling_event = self.tracker.create_new_event(name="throttling") + + throttling_event.set_event_param(name="provider", value=provider) + throttling_event.set_event_param(name="exception_name", value=exception_name) + throttling_event.set_event_param(name="exception_info", value=exception_info) + + try: + self.tracker.send(events=[throttling_event]) + except Exception: + logging.debug("BAZARR unable to track event.") + else: + self.tracker.store.save() + event_tracker = EventTracker() diff --git a/frontend/src/apis/hooks/providers.ts b/frontend/src/apis/hooks/providers.ts index f1daf9f37..c804a9a8e 100644 --- a/frontend/src/apis/hooks/providers.ts +++ b/frontend/src/apis/hooks/providers.ts @@ -18,7 +18,7 @@ export function useMoviesProvider(radarrId?: number) { } }, { - staleTime: Infinity, + staleTime: 0, } ); } @@ -32,7 +32,7 @@ export function useEpisodesProvider(episodeId?: number) { } }, { - staleTime: Infinity, + staleTime: 0, } ); } diff --git a/frontend/src/components/forms/TimeOffsetForm.tsx b/frontend/src/components/forms/TimeOffsetForm.tsx index b02e75781..6d213b359 100644 --- a/frontend/src/components/forms/TimeOffsetForm.tsx +++ b/frontend/src/components/forms/TimeOffsetForm.tsx @@ -83,12 +83,25 @@ const TimeOffsetForm: FunctionComponent = ({ selections, onSubmit }) => { > - - - + + + ); diff --git a/frontend/src/pages/Settings/Subtitles/index.tsx b/frontend/src/pages/Settings/Subtitles/index.tsx index 67920e3fb..0d0337201 100644 --- a/frontend/src/pages/Settings/Subtitles/index.tsx +++ b/frontend/src/pages/Settings/Subtitles/index.tsx @@ -215,8 +215,8 @@ const SettingsSubtitlesView: FunctionComponent = () => { settingKey="settings-general-adaptive_searching" > - When searching for subtitles, Bazarr will reduce search frequency to - limit call to providers. + When enabled, Bazarr will skip searching providers for subtitles which + have been searched recently. { options={adaptiveSearchingDelayOption} > - In order to reduce search frequency, how many weeks must Bazarr wait - after initial search. + The delay from the first search to adaptive searching applying. + During this window Bazarr will continue to search for subtitles, + even if they have been searched for recently. { options={adaptiveSearchingDeltaOption} > - How often should Bazarr search for subtitles when in adaptive search - mode. + The delay between Bazarr searching for subtitles in adaptive search + mode. If the media has been searched for more recently than this + value, Bazarr will skip searching for subtitles.