1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 17:06:17 +00:00

Refactored star/unstar error handling

This commit is contained in:
spl0k 2018-02-28 20:12:34 +01:00
parent 5188976e6f
commit 8bf488fab2
2 changed files with 87 additions and 84 deletions

View File

@ -18,6 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import time import time
import uuid import uuid
@ -32,110 +33,85 @@ from ..lastfm import LastFm
from ..py23 import dict from ..py23 import dict
from . import api, get_entity from . import api, get_entity
from .exceptions import GenericError, MissingParameter, NotFound from .exceptions import AggregateException, GenericError, MissingParameter, NotFound
def try_star(cls, starred_cls, eid): def star_single(cls, eid):
""" Stars an entity """ Stars an entity
:param cls: entity class, Folder, Artist, Album or Track :param cls: entity class, Folder, Artist, Album or Track
:param starred_cls: class used for the db representation of the starring of ent
:param eid: id of the entity to star :param eid: id of the entity to star
:return error dict, if any. None otherwise
""" """
try: uid = uuid.UUID(eid)
uid = uuid.UUID(eid) e = cls[uid]
e = cls[uid]
except ValueError:
return dict(code = 0, message = 'Invalid {} id {}'.format(cls.__name__, eid))
except ObjectNotFound:
return dict(code = 70, message = 'Unknown {} id {}'.format(cls.__name__, eid))
starred_cls = getattr(sys.modules[__name__], 'Starred' + cls.__name__)
try: try:
starred_cls[request.user, uid] starred_cls[request.user, uid]
return dict(code = 0, message = '{} {} already starred'.format(cls.__name__, eid)) raise GenericError('{} {} already starred'.format(cls.__name__, eid))
except ObjectNotFound: except ObjectNotFound:
pass pass
starred_cls(user = request.user, starred = e) starred_cls(user = request.user, starred = e)
return None
def try_unstar(starred_cls, eid): def unstar_single(cls, eid):
""" Unstars an entity """ Unstars an entity
:param starred_cls: class used for the db representation of the starring of the entity :param cls: entity class, Folder, Artist, Album or Track
:param eid: id of the entity to unstar :param eid: id of the entity to unstar
:return error dict, if any. None otherwise
""" """
try: uid = uuid.UUID(eid)
uid = uuid.UUID(eid) starred_cls = getattr(sys.modules[__name__], 'Starred' + cls.__name__)
except ValueError:
return dict(code = 0, message = 'Invalid id {}'.format(eid))
delete(s for s in starred_cls if s.user.id == request.user.id and s.starred.id == uid) delete(s for s in starred_cls if s.user.id == request.user.id and s.starred.id == uid)
return None return None
def merge_errors(errors): def handle_star_request(func):
error = None id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ])
errors = [ e for e in errors if e ]
if len(errors) == 1:
error = errors[0]
elif len(errors) > 1:
codes = set(map(lambda e: e['code'], errors))
error = dict(code = list(codes)[0] if len(codes) == 1 else 0, error = errors)
return error if not id and not albumId and not artistId:
raise MissingParameter('id, albumId or artistId')
errors = []
for eid in id:
terr = None
ferr = None
try:
func(Track, eid)
except Exception as e:
terr = e
try:
func(Folder, eid)
except Exception as e:
ferr = e
if terr and ferr:
errors += [ terr, ferr ]
for alId in albumId:
try:
func(Album, alId)
except Exception as e:
errors.append(e)
for arId in artistId:
try:
func(Artist, arId)
except Exception as e:
errors.append(e)
if errors:
raise AggregateException(errors)
return request.formatter.empty
@api.route('/star.view', methods = [ 'GET', 'POST' ]) @api.route('/star.view', methods = [ 'GET', 'POST' ])
def star(): def star():
id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ]) return handle_star_request(star_single)
if not id and not albumId and not artistId:
raise MissingParameter('id, albumId or artistId')
errors = []
for eid in id:
terr = try_star(Track, StarredTrack, eid)
ferr = try_star(Folder, StarredFolder, eid)
if terr and ferr:
errors += [ terr, ferr ]
for alId in albumId:
errors.append(try_star(Album, StarredAlbum, alId))
for arId in artistId:
errors.append(try_star(Artist, StarredArtist, arId))
error = merge_errors(errors)
if error:
return request.formatter('error', error)
return request.formatter.empty
@api.route('/unstar.view', methods = [ 'GET', 'POST' ]) @api.route('/unstar.view', methods = [ 'GET', 'POST' ])
def unstar(): def unstar():
id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ]) return handle_star_request(unstar_single)
if not id and not albumId and not artistId:
raise MissingParameter('id, albumId or artistId')
errors = []
for eid in id:
terr = try_unstar(StarredTrack, eid)
ferr = try_unstar(StarredFolder, eid)
if terr and ferr:
errors += [ terr, ferr ]
for alId in albumId:
errors.append(try_unstar(StarredAlbum, alId))
for arId in artistId:
errors.append(try_unstar(StarredArtist, arId))
error = merge_errors(errors)
if error:
return request.formatter('error', error)
return request.formatter.empty
@api.route('/setRating.view', methods = [ 'GET', 'POST' ]) @api.route('/setRating.view', methods = [ 'GET', 'POST' ])
def rate(): def rate():

View File

@ -7,10 +7,10 @@
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
from flask import request from flask import current_app, request
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
class SubsonicAPIError(HTTPException): class SubsonicAPIException(HTTPException):
code = 400 code = 400
api_code = None api_code = None
message = None message = None
@ -24,7 +24,7 @@ class SubsonicAPIError(HTTPException):
code = self.api_code if self.api_code is not None else '??' code = self.api_code if self.api_code is not None else '??'
return '{}: {}'.format(code, self.message) return '{}: {}'.format(code, self.message)
class GenericError(SubsonicAPIError): class GenericError(SubsonicAPIException):
api_code = 0 api_code = 0
def __init__(self, message, *args, **kwargs): def __init__(self, message, *args, **kwargs):
@ -34,40 +34,40 @@ class GenericError(SubsonicAPIError):
class ServerError(GenericError): class ServerError(GenericError):
code = 500 code = 500
class MissingParameter(SubsonicAPIError): class MissingParameter(SubsonicAPIException):
api_code = 10 api_code = 10
def __init__(self, param, *args, **kwargs): def __init__(self, param, *args, **kwargs):
super(MissingParameter, self).__init__(*args, **kwargs) super(MissingParameter, self).__init__(*args, **kwargs)
self.message = "Required parameter '{}' is missing.".format(param) self.message = "Required parameter '{}' is missing.".format(param)
class ClientMustUpgrade(SubsonicAPIError): class ClientMustUpgrade(SubsonicAPIException):
api_code = 20 api_code = 20
message = 'Incompatible Subsonic REST protocol version. Client must upgrade.' message = 'Incompatible Subsonic REST protocol version. Client must upgrade.'
class ServerMustUpgrade(SubsonicAPIError): class ServerMustUpgrade(SubsonicAPIException):
code = 501 code = 501
api_code = 30 api_code = 30
message = 'Incompatible Subsonic REST protocol version. Server must upgrade.' message = 'Incompatible Subsonic REST protocol version. Server must upgrade.'
class Unauthorized(SubsonicAPIError): class Unauthorized(SubsonicAPIException):
code = 401 code = 401
api_code = 40 api_code = 40
message = 'Wrong username or password.' message = 'Wrong username or password.'
class Forbidden(SubsonicAPIError): class Forbidden(SubsonicAPIException):
code = 403 code = 403
api_code = 50 api_code = 50
message = 'User is not authorized for the given operation.' message = 'User is not authorized for the given operation.'
class TrialExpired(SubsonicAPIError): class TrialExpired(SubsonicAPIException):
code = 402 code = 402
api_code = 60 api_code = 60
message = ("The trial period for the Supysonic server is over." message = ("The trial period for the Supysonic server is over."
"But since it doesn't use any licensing you shouldn't be seeing this error ever." "But since it doesn't use any licensing you shouldn't be seeing this error ever."
"So something went wrong or you got scammed.") "So something went wrong or you got scammed.")
class NotFound(SubsonicAPIError): class NotFound(SubsonicAPIException):
code = 404 code = 404
api_code = 70 api_code = 70
@ -75,3 +75,30 @@ class NotFound(SubsonicAPIError):
super(NotFound, self).__init__(*args, **kwargs) super(NotFound, self).__init__(*args, **kwargs)
self.message = '{} not found'.format(entity) self.message = '{} not found'.format(entity)
class AggregateException(SubsonicAPIException):
def __init__(self, exceptions, *args, **kwargs):
super(AggregateException, self).__init__(*args, **kwargs)
self.exceptions = []
for exc in exceptions:
if not isinstance(exc, SubsonicAPIException):
# Try to convert regular exceptions to SubsonicAPIExceptions
handler = current_app._find_error_handler(exc) # meh
if handler:
exc = handler(exc)
assert isinstance(exc, SubsonicAPIException)
else:
exc = GenericError(str(exc))
self.exceptions.append(exc)
def get_response(self, environ = None):
if len(self.exceptions) == 1:
return self.exceptions[0].get_response()
codes = set(exc.api_code for exc in self.exceptions)
errors = [ dict(code = exc.api_code, message = exc.message) for exc in self.exceptions ]
rv = request.formatter('error', dict(code = list(codes)[0] if len(codes) == 1 else 0, error = errors))
rv.status_code = self.code
return rv