From 1605fcd2027c1bbbbc5b6b0d951fb59eb449de27 Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 6 Jan 2018 00:33:45 +0100 Subject: [PATCH] Py3: imports, exceptions, dicts Ref #75 --- README.md | 1 + requirements.txt | 1 + supysonic/api/__init__.py | 100 +++++++++---------- supysonic/api/albums_songs.py | 96 ++++++++++--------- supysonic/api/annotation.py | 22 +++-- supysonic/api/browse.py | 86 +++++++++-------- supysonic/api/chat.py | 8 +- supysonic/api/media.py | 28 +++--- supysonic/api/playlists.py | 14 +-- supysonic/api/search.py | 45 +++++---- supysonic/api/system.py | 8 +- supysonic/api/user.py | 14 +-- supysonic/cli.py | 8 +- supysonic/config.py | 9 +- supysonic/db.py | 153 +++++++++++++++--------------- supysonic/frontend/user.py | 10 +- supysonic/lastfm.py | 6 +- supysonic/watcher.py | 12 ++- supysonic/web.py | 4 +- tests/api/test_response_helper.py | 4 +- tests/base/test_cli.py | 8 +- tests/testbase.py | 4 +- 22 files changed, 341 insertions(+), 300 deletions(-) diff --git a/README.md b/README.md index 2757ccf..03cb710 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ To install it, run: You'll need these to run Supysonic: * Python 2.7 +* [future](http://python-future.org/) * [Flask](http://flask.pocoo.org/) >= 0.9 * [PonyORM](https://ponyorm.com/) * [Python Imaging Library](https://github.com/python-pillow/Pillow) diff --git a/requirements.txt b/requirements.txt index d048741..c75d3ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flask>=0.9 +future pony Pillow simplejson diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py index f35f647..0a7923e 100644 --- a/supysonic/api/__init__.py +++ b/supysonic/api/__init__.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -29,6 +29,8 @@ from xml.etree import ElementTree from ..managers.user import UserManager +from builtins import dict + @app.before_request def set_formatter(): if not request.path.startswith('/rest/'): @@ -39,19 +41,19 @@ def set_formatter(): if f == 'jsonp': # Some clients (MiniSub, Perisonic) set f to jsonp without callback for streamed data if not callback and request.endpoint not in [ 'stream_media', 'cover_art' ]: - return ResponseHelper.responsize_json({ - 'error': { - 'code': 10, - 'message': 'Missing callback' - } - }, error = True), 400 + return ResponseHelper.responsize_json(dict( + error = dict( + code = 10, + message = 'Missing callback' + ) + ), error = True), 400 request.formatter = lambda x, **kwargs: ResponseHelper.responsize_jsonp(x, callback, kwargs) elif f == "json": request.formatter = ResponseHelper.responsize_json else: request.formatter = ResponseHelper.responsize_xml - request.error_formatter = lambda code, msg: request.formatter({ 'error': { 'code': code, 'message': msg } }, error = True) + request.error_formatter = lambda code, msg: request.formatter(dict(error = dict(code = code, message = msg)), error = True) def decode_password(password): if not password.startswith('enc:'): @@ -134,24 +136,29 @@ def not_found(error): class ResponseHelper: @staticmethod - def responsize_json(ret, error = False, version = "1.8.0"): - def check_lists(d): - for key, value in d.items(): - if isinstance(value, dict): - d[key] = check_lists(value) - elif isinstance(value, list): - if len(value) == 0: - del d[key] - else: - d[key] = [ check_lists(item) if isinstance(item, dict) else item for item in value ] - return d + def remove_empty_lists(d): + if not isinstance(d, dict): + raise TypeError('Expecting a dict') + + for key, value in d.items(): + if isinstance(value, dict): + d[key] = ResponseHelper.remove_empty_lists(value) + elif isinstance(value, list): + if len(value) == 0: + del d[key] + else: + d[key] = [ ResponseHelper.remove_empty_lists(item) if isinstance(item, dict) else item for item in value ] + return d + + @staticmethod + def responsize_json(ret, error = False, version = "1.8.0"): + ret = ResponseHelper.remove_empty_lists(ret) - ret = check_lists(ret) # add headers to response - ret.update({ - 'status': 'failed' if error else 'ok', - 'version': version - }) + ret.update( + status = 'failed' if error else 'ok', + version = version + ) return simplejson.dumps({ 'subsonic-response': ret }, indent = True, encoding = 'utf-8') @staticmethod @@ -161,11 +168,11 @@ class ResponseHelper: @staticmethod def responsize_xml(ret, error = False, version = "1.8.0"): """Return an xml response from json and replace unsupported characters.""" - ret.update({ - 'status': 'failed' if error else 'ok', - 'version': version, - 'xmlns': "http://subsonic.org/restapi" - }) + ret.update( + status = 'failed' if error else 'ok', + version = version, + xmlns = "http://subsonic.org/restapi" + ) elem = ElementTree.Element('subsonic-response') ResponseHelper.dict2xml(elem, ret) @@ -184,27 +191,24 @@ class ResponseHelper: """ if not isinstance(dictionary, dict): raise TypeError('Expecting a dict') - if not all(map(lambda x: isinstance(x, basestring), dictionary.keys())): + if not all(map(lambda x: isinstance(x, basestring), dictionary)): raise TypeError('Dictionary keys must be strings') - subelems = { k: v for k, v in dictionary.iteritems() if isinstance(v, dict) } - sequences = { k: v for k, v in dictionary.iteritems() if isinstance(v, list) } - attributes = { k: v for k, v in dictionary.iteritems() if k != '_value_' and k not in subelems and k not in sequences } - - if '_value_' in dictionary: - elem.text = ResponseHelper.value_tostring(dictionary['_value_']) - for attr, value in attributes.iteritems(): - elem.set(attr, ResponseHelper.value_tostring(value)) - for sub, subdict in subelems.iteritems(): - subelem = ElementTree.SubElement(elem, sub) - ResponseHelper.dict2xml(subelem, subdict) - for seq, values in sequences.iteritems(): - for value in values: - subelem = ElementTree.SubElement(elem, seq) - if isinstance(value, dict): - ResponseHelper.dict2xml(subelem, value) - else: - subelem.text = ResponseHelper.value_tostring(value) + for name, value in dictionary.items(): + if name == '_value_': + elem.text = ResponseHelper.value_tostring(value) + elif isinstance(value, dict): + subelem = ElementTree.SubElement(elem, name) + ResponseHelper.dict2xml(subelem, value) + elif isinstance(value, list): + for v in value: + subelem = ElementTree.SubElement(elem, name) + if isinstance(v, dict): + ResponseHelper.dict2xml(subelem, v) + else: + subelem.text = ResponseHelper.value_tostring(v) + else: + elem.set(name, ResponseHelper.value_tostring(value)) @staticmethod def value_tostring(value): diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py index 8c453ba..24f4b21 100644 --- a/supysonic/api/albums_songs.py +++ b/supysonic/api/albums_songs.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -28,6 +28,8 @@ from pony.orm import db_session, select, desc, avg, max, min, count from ..db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User from ..db import now +from builtins import dict + @app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ]) def rand_songs(): size = request.values.get('size', '10') @@ -56,11 +58,11 @@ def rand_songs(): query = query.filter(lambda t: t.root_folder.id == fid) with db_session: - return request.formatter({ - 'randomSongs': { - 'song': [ t.as_subsonic_child(request.user, request.client) for t in query.random(size) ] - } - }) + return request.formatter(dict( + randomSongs = dict( + song = [ t.as_subsonic_child(request.user, request.client) for t in query.random(size) ] + ) + )) @app.route('/rest/getAlbumList.view', methods = [ 'GET', 'POST' ]) def album_list(): @@ -76,11 +78,11 @@ def album_list(): query = select(t.folder for t in Track) if ltype == 'random': with db_session: - return request.formatter({ - 'albumList': { - 'album': [ a.as_subsonic_child(request.user) for a in query.random(size) ] - } - }) + return request.formatter(dict( + albumList = dict( + album = [ a.as_subsonic_child(request.user) for a in query.random(size) ] + ) + )) elif ltype == 'newest': query = query.order_by(desc(Folder.created)) elif ltype == 'highest': @@ -99,11 +101,11 @@ def album_list(): return request.error_formatter(0, 'Unknown search type') with db_session: - return request.formatter({ - 'albumList': { - 'album': [ f.as_subsonic_child(request.user) for f in query.limit(size, offset) ] - } - }) + return request.formatter(dict( + albumList = dict( + album = [ f.as_subsonic_child(request.user) for f in query.limit(size, offset) ] + ) + )) @app.route('/rest/getAlbumList2.view', methods = [ 'GET', 'POST' ]) def album_list_id3(): @@ -119,11 +121,11 @@ def album_list_id3(): query = Album.select() if ltype == 'random': with db_session: - return request.formatter({ - 'albumList2': { - 'album': [ a.as_subsonic_album(request.user) for a in query.random(size) ] - } - }) + return request.formatter(dict( + albumList2 = dict( + album = [ a.as_subsonic_album(request.user) for a in query.random(size) ] + ) + )) elif ltype == 'newest': query = query.order_by(lambda a: desc(min(a.tracks.created))) elif ltype == 'frequent': @@ -140,47 +142,47 @@ def album_list_id3(): return request.error_formatter(0, 'Unknown search type') with db_session: - return request.formatter({ - 'albumList2': { - 'album': [ f.as_subsonic_album(request.user) for f in query.limit(size, offset) ] - } - }) + return request.formatter(dict( + albumList2 = dict( + album = [ f.as_subsonic_album(request.user) for f in query.limit(size, offset) ] + ) + )) @app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ]) @db_session def now_playing(): query = User.select(lambda u: u.last_play is not None and u.last_play_date + timedelta(minutes = 3) > now()) - return request.formatter({ - 'nowPlaying': { - 'entry': [ dict( - u.last_play.as_subsonic_child(request.user, request.client).items() + - { 'username': u.name, 'minutesAgo': (now() - u.last_play_date).seconds / 60, 'playerId': 0 }.items() + return request.formatter(dict( + nowPlaying = dict( + entry = [ dict( + u.last_play.as_subsonic_child(request.user, request.client), + username = u.name, minutesAgo = (now() - u.last_play_date).seconds / 60, playerId = 0 ) for u in query ] - } - }) + ) + )) @app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ]) @db_session def get_starred(): folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id) - return request.formatter({ - 'starred': { - 'artist': [ { 'id': str(sf.id), 'name': sf.name } for sf in folders.filter(lambda f: count(f.tracks) == 0) ], - 'album': [ sf.as_subsonic_child(request.user) for sf in folders.filter(lambda f: count(f.tracks) > 0) ], - 'song': [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ] - } - }) + return request.formatter(dict( + starred = dict( + artist = [ dict(id = str(sf.id), name = sf.name) for sf in folders.filter(lambda f: count(f.tracks) == 0) ], + album = [ sf.as_subsonic_child(request.user) for sf in folders.filter(lambda f: count(f.tracks) > 0) ], + song = [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ] + ) + )) @app.route('/rest/getStarred2.view', methods = [ 'GET', 'POST' ]) @db_session def get_starred_id3(): - return request.formatter({ - 'starred2': { - 'artist': [ sa.as_subsonic_artist(request.user) for sa in select(s.starred for s in StarredArtist if s.user.id == request.user.id) ], - 'album': [ sa.as_subsonic_album(request.user) for sa in select(s.starred for s in StarredAlbum if s.user.id == request.user.id) ], - 'song': [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ] - } - }) + return request.formatter(dict( + starred2 = dict( + artist = [ sa.as_subsonic_artist(request.user) for sa in select(s.starred for s in StarredArtist if s.user.id == request.user.id) ], + album = [ sa.as_subsonic_album(request.user) for sa in select(s.starred for s in StarredAlbum if s.user.id == request.user.id) ], + song = [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ] + ) + )) diff --git a/supysonic/api/annotation.py b/supysonic/api/annotation.py index 3bad417..ac5420b 100644 --- a/supysonic/api/annotation.py +++ b/supysonic/api/annotation.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -32,6 +32,8 @@ from ..lastfm import LastFm from . import get_entity +from builtins import dict + @db_session def try_star(cls, starred_cls, eid): """ Stars an entity @@ -45,16 +47,16 @@ def try_star(cls, starred_cls, eid): try: uid = uuid.UUID(eid) except: - return { 'code': 0, 'message': 'Invalid {} id {}'.format(cls.__name__, eid) } + return dict(code = 0, message = 'Invalid {} id {}'.format(cls.__name__, eid)) try: e = cls[uid] except ObjectNotFound: - return { 'code': 70, 'message': 'Unknown {} id {}'.format(cls.__name__, eid) } + return dict(code = 70, message = 'Unknown {} id {}'.format(cls.__name__, eid)) try: starred_cls[request.user.id, uid] - return { 'code': 0, 'message': '{} {} already starred'.format(cls.__name__, eid) } + return dict(code = 0, message = '{} {} already starred'.format(cls.__name__, eid)) except ObjectNotFound: pass @@ -73,7 +75,7 @@ def try_unstar(starred_cls, eid): try: uid = uuid.UUID(eid) except: - return { 'code': 0, 'message': 'Invalid id {}'.format(eid) } + 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) return None @@ -85,7 +87,7 @@ def merge_errors(errors): error = errors[0] elif len(errors) > 1: codes = set(map(lambda e: e['code'], errors)) - error = { 'code': list(codes)[0] if len(codes) == 1 else 0, 'error': errors } + error = dict(code = list(codes)[0] if len(codes) == 1 else 0, error = errors) return error @@ -110,7 +112,7 @@ def star(): errors.append(try_star(Artist, StarredArtist, arId)) error = merge_errors(errors) - return request.formatter({ 'error': error }, error = True) if error else request.formatter({}) + return request.formatter(dict(error = error), error = True) if error else request.formatter(dict()) @app.route('/rest/unstar.view', methods = [ 'GET', 'POST' ]) def unstar(): @@ -133,7 +135,7 @@ def unstar(): errors.append(try_unstar(StarredArtist, arId)) error = merge_errors(errors) - return request.formatter({ 'error': error }, error = True) if error else request.formatter({}) + return request.formatter(dict(error = error), error = True) if error else request.formatter(dict()) @app.route('/rest/setRating.view', methods = [ 'GET', 'POST' ]) def rate(): @@ -171,7 +173,7 @@ def rate(): except ObjectNotFound: rating_cls(user = User[request.user.id], rated = rated, rating = rating) - return request.formatter({}) + return request.formatter(dict()) @app.route('/rest/scrobble.view', methods = [ 'GET', 'POST' ]) @db_session @@ -197,5 +199,5 @@ def scrobble(): else: lfm.now_playing(res) - return request.formatter({}) + return request.formatter(dict()) diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py index ac39dee..00584b0 100644 --- a/supysonic/api/browse.py +++ b/supysonic/api/browse.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -29,17 +29,19 @@ from ..db import Folder, Artist, Album, Track from . import get_entity +from builtins import dict + @app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ]) @db_session def list_folders(): - return request.formatter({ - 'musicFolders': { - 'musicFolder': [ { - 'id': str(f.id), - 'name': f.name - } for f in Folder.select(lambda f: f.root).order_by(Folder.name) ] - } - }) + return request.formatter(dict( + musicFolders = dict( + musicFolder = [ dict( + id = str(f.id), + name = f.name + ) for f in Folder.select(lambda f: f.root).order_by(Folder.name) ] + ) + )) @app.route('/rest/getIndexes.view', methods = [ 'GET', 'POST' ]) @db_session @@ -70,7 +72,7 @@ def list_indexes(): last_modif = max(map(lambda f: f.last_scan, folders)) if ifModifiedSince is not None and last_modif < ifModifiedSince: - return request.formatter({ 'indexes': { 'lastModified': last_modif * 1000 } }) + return request.formatter(dict(indexes = dict(lastModified = last_modif * 1000))) # The XSD lies, we don't return artists but a directory structure artists = [] @@ -79,7 +81,7 @@ def list_indexes(): artists += f.children.select()[:] children += f.tracks.select()[:] - indexes = {} + indexes = dict() for artist in artists: index = artist.name[0].upper() if index in map(str, xrange(10)): @@ -92,19 +94,19 @@ def list_indexes(): indexes[index].append(artist) - return request.formatter({ - 'indexes': { - 'lastModified': last_modif * 1000, - 'index': [ { - 'name': k, - 'artist': [ { - 'id': str(a.id), - 'name': a.name - } for a in sorted(v, key = lambda a: a.name.lower()) ] - } for k, v in sorted(indexes.iteritems()) ], - 'child': [ c.as_subsonic_child(request.user, request.client) for c in sorted(children, key = lambda t: t.sort_key()) ] - } - }) + return request.formatter(dict( + indexes = dict( + lastModified = last_modif * 1000, + index = [ dict( + name = k, + artist = [ dict( + id = str(a.id), + name = a.name + ) for a in sorted(v, key = lambda a: a.name.lower()) ] + ) for k, v in sorted(indexes.items()) ], + child = [ c.as_subsonic_child(request.user, request.client) for c in sorted(children, key = lambda t: t.sort_key()) ] + ) + )) @app.route('/rest/getMusicDirectory.view', methods = [ 'GET', 'POST' ]) @db_session @@ -113,21 +115,21 @@ def show_directory(): if not status: return res - directory = { - 'id': str(res.id), - 'name': res.name, - 'child': [ f.as_subsonic_child(request.user) for f in res.children.order_by(lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user, request.client) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] - } + directory = dict( + id = str(res.id), + name = res.name, + child = [ f.as_subsonic_child(request.user) for f in res.children.order_by(lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user, request.client) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] + ) if not res.root: directory['parent'] = str(res.parent.id) - return request.formatter({ 'directory': directory }) + return request.formatter(dict(directory = directory)) @app.route('/rest/getArtists.view', methods = [ 'GET', 'POST' ]) @db_session def list_artists(): # According to the API page, there are no parameters? - indexes = {} + indexes = dict() for artist in Artist.select(): index = artist.name[0].upper() if artist.name else '?' if index in map(str, xrange(10)): @@ -140,14 +142,14 @@ def list_artists(): indexes[index].append(artist) - return request.formatter({ - 'artists': { - 'index': [ { - 'name': k, - 'artist': [ a.as_subsonic_artist(request.user) for a in sorted(v, key = lambda a: a.name.lower()) ] - } for k, v in sorted(indexes.iteritems()) ] - } - }) + return request.formatter(dict( + artists = dict( + index = [ dict( + name = k, + artist = [ a.as_subsonic_artist(request.user) for a in sorted(v, key = lambda a: a.name.lower()) ] + ) for k, v in sorted(indexes.items()) ] + ) + )) @app.route('/rest/getArtist.view', methods = [ 'GET', 'POST' ]) @db_session @@ -161,7 +163,7 @@ def artist_info(): albums |= { t.album for t in res.tracks } info['album'] = [ a.as_subsonic_album(request.user) for a in sorted(albums, key = lambda a: a.sort_key()) ] - return request.formatter({ 'artist': info }) + return request.formatter(dict(artist = info)) @app.route('/rest/getAlbum.view', methods = [ 'GET', 'POST' ]) @db_session @@ -173,7 +175,7 @@ def album_info(): info = res.as_subsonic_album(request.user) info['song'] = [ t.as_subsonic_child(request.user, request.client) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] - return request.formatter({ 'album': info }) + return request.formatter(dict(album = info)) @app.route('/rest/getSong.view', methods = [ 'GET', 'POST' ]) @db_session @@ -182,7 +184,7 @@ def track_info(): if not status: return res - return request.formatter({ 'song': res.as_subsonic_child(request.user, request.client) }) + return request.formatter(dict(song = res.as_subsonic_child(request.user, request.client))) @app.route('/rest/getVideos.view', methods = [ 'GET', 'POST' ]) def list_videos(): diff --git a/supysonic/api/chat.py b/supysonic/api/chat.py index 9f5cdef..f7f41ab 100644 --- a/supysonic/api/chat.py +++ b/supysonic/api/chat.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -23,6 +23,8 @@ from pony.orm import db_session from ..db import ChatMessage, User +from builtins import dict + @app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ]) def get_chat(): since = request.values.get('since') @@ -36,7 +38,7 @@ def get_chat(): if since: query = query.filter(lambda m: m.time > since) - return request.formatter({ 'chatMessages': { 'chatMessage': [ msg.responsize() for msg in query ] }}) + return request.formatter(dict(chatMessages = dict(chatMessage = [ msg.responsize() for msg in query ] ))) @app.route('/rest/addChatMessage.view', methods = [ 'GET', 'POST' ]) def add_chat_message(): @@ -47,5 +49,5 @@ def add_chat_message(): with db_session: ChatMessage(user = User[request.user.id], message = msg) - return request.formatter({}) + return request.formatter(dict()) diff --git a/supysonic/api/media.py b/supysonic/api/media.py index 8531087..3ab8ffa 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -34,6 +34,8 @@ from ..db import Track, Album, Artist, Folder, User, ClientPrefs, now from . import get_entity +from builtins import dict + def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate): if not base_cmdline: return None @@ -195,11 +197,11 @@ def lyrics(): app.logger.warn('Unsupported encoding for lyrics file ' + lyrics_path) continue - return request.formatter({ 'lyrics': { - 'artist': track.album.artist.name, - 'title': track.title, - '_value_': lyrics - } }) + return request.formatter(dict(lyrics = dict( + artist = track.album.artist.name, + title = track.title, + _value_ = lyrics + ))) try: r = requests.get("http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect", @@ -207,15 +209,15 @@ def lyrics(): root = ElementTree.fromstring(r.content) ns = { 'cl': 'http://api.chartlyrics.com/' } - return request.formatter({ 'lyrics': { - 'artist': root.find('cl:LyricArtist', namespaces = ns).text, - 'title': root.find('cl:LyricSong', namespaces = ns).text, - '_value_': root.find('cl:Lyric', namespaces = ns).text - } }) - except requests.exceptions.RequestException, e: + return request.formatter(dict(lyrics = dict( + artist = root.find('cl:LyricArtist', namespaces = ns).text, + title = root.find('cl:LyricSong', namespaces = ns).text, + _value_ = root.find('cl:Lyric', namespaces = ns).text + ))) + except requests.exceptions.RequestException as e: app.logger.warn('Error while requesting the ChartLyrics API: ' + str(e)) - return request.formatter({ 'lyrics': {} }) + return request.formatter(dict(lyrics = dict())) def read_file_as_unicode(path): """ Opens a file trying with different encodings and returns the contents as a unicode string """ diff --git a/supysonic/api/playlists.py b/supysonic/api/playlists.py index 8810d23..bc7bf90 100644 --- a/supysonic/api/playlists.py +++ b/supysonic/api/playlists.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -28,6 +28,8 @@ from ..db import Playlist, User, Track from . import get_entity +from builtins import dict + @app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ]) def list_playlists(): query = Playlist.select(lambda p: p.user.id == request.user.id or p.public).order_by(Playlist.name) @@ -45,7 +47,7 @@ def list_playlists(): query = Playlist.select(lambda p: p.user.name == username).order_by(Playlist.name) with db_session: - return request.formatter({ 'playlists': { 'playlist': [ p.as_subsonic_playlist(request.user) for p in query ] } }) + return request.formatter(dict(playlists = dict(playlist = [ p.as_subsonic_playlist(request.user) for p in query ] ))) @app.route('/rest/getPlaylist.view', methods = [ 'GET', 'POST' ]) @db_session @@ -59,7 +61,7 @@ def show_playlist(): info = res.as_subsonic_playlist(request.user) info['entry'] = [ t.as_subsonic_child(request.user, request.client) for t in res.get_tracks() ] - return request.formatter({ 'playlist': info }) + return request.formatter(dict(playlist = info)) @app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ]) @db_session @@ -99,7 +101,7 @@ def create_playlist(): playlist.add(track) - return request.formatter({}) + return request.formatter(dict()) @app.route('/rest/deletePlaylist.view', methods = [ 'GET', 'POST' ]) @db_session @@ -112,7 +114,7 @@ def delete_playlist(): return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours") res.delete() - return request.formatter({}) + return request.formatter(dict()) @app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ]) @db_session @@ -149,5 +151,5 @@ def update_playlist(): playlist.remove_at_indexes(to_remove) - return request.formatter({}) + return request.formatter(dict()) diff --git a/supysonic/api/search.py b/supysonic/api/search.py index 07c61b0..23b2fdd 100644 --- a/supysonic/api/search.py +++ b/supysonic/api/search.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -18,12 +18,15 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from collections import OrderedDict from datetime import datetime from flask import request, current_app as app from pony.orm import db_session, select from ..db import Folder, Track, Artist, Album +from builtins import dict + @app.route('/rest/search.view', methods = [ 'GET', 'POST' ]) def old_search(): artist, album, title, anyf, count, offset, newer_than = map(request.values.get, [ 'artist', 'album', 'title', 'any', 'count', 'offset', 'newerThan' ]) @@ -53,20 +56,20 @@ def old_search(): tend = offset + count - fcount res += tracks[toff : tend] - return request.formatter({ 'searchResult': { - 'totalHits': folders.count() + tracks.count(), - 'offset': offset, - 'match': [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.client) for r in res ] - }}) + return request.formatter(dict(searchResult = dict( + totalHits = folders.count() + tracks.count(), + offset = offset, + match = [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.client) for r in res ] + ))) else: return request.error_formatter(10, 'Missing search parameter') with db_session: - return request.formatter({ 'searchResult': { - 'totalHits': query.count(), - 'offset': offset, - 'match': [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.client) for r in query[offset : offset + count] ] - }}) + return request.formatter(dict(searchResult = dict( + totalHits = query.count(), + offset = offset, + match = [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.client) for r in query[offset : offset + count] ] + ))) @app.route('/rest/search2.view', methods = [ 'GET', 'POST' ]) def new_search(): @@ -91,11 +94,11 @@ def new_search(): albums = select(t.folder for t in Track if query in t.folder.name).limit(album_count, album_offset) songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset) - return request.formatter({ 'searchResult2': { - 'artist': [ { 'id': str(a.id), 'name': a.name } for a in artists ], - 'album': [ f.as_subsonic_child(request.user) for f in albums ], - 'song': [ t.as_subsonic_child(request.user, request.client) for t in songs ] - }}) + return request.formatter(dict(searchResult2 = OrderedDict( + artist = [ dict(id = str(a.id), name = a.name) for a in artists ], + album = [ f.as_subsonic_child(request.user) for f in albums ], + song = [ t.as_subsonic_child(request.user, request.client) for t in songs ] + ))) @app.route('/rest/search3.view', methods = [ 'GET', 'POST' ]) def search_id3(): @@ -120,9 +123,9 @@ def search_id3(): albums = Album.select(lambda a: query in a.name).limit(album_count, album_offset) songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset) - return request.formatter({ 'searchResult3': { - 'artist': [ a.as_subsonic_artist(request.user) for a in artists ], - 'album': [ a.as_subsonic_album(request.user) for a in albums ], - 'song': [ t.as_subsonic_child(request.user, request.client) for t in songs ] - }}) + return request.formatter(dict(searchResult3 = OrderedDict( + artist = [ a.as_subsonic_artist(request.user) for a in artists ], + album = [ a.as_subsonic_album(request.user) for a in albums ], + song = [ t.as_subsonic_child(request.user, request.client) for t in songs ] + ))) diff --git a/supysonic/api/system.py b/supysonic/api/system.py index fde270c..82e5707 100644 --- a/supysonic/api/system.py +++ b/supysonic/api/system.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -20,11 +20,13 @@ from flask import request, current_app as app +from builtins import dict + @app.route('/rest/ping.view', methods = [ 'GET', 'POST' ]) def ping(): - return request.formatter({}) + return request.formatter(dict()) @app.route('/rest/getLicense.view', methods = [ 'GET', 'POST' ]) def license(): - return request.formatter({ 'license': { 'valid': True } }) + return request.formatter(dict(license = dict(valid = True ))) diff --git a/supysonic/api/user.py b/supysonic/api/user.py index 43ca1e1..4b66c53 100644 --- a/supysonic/api/user.py +++ b/supysonic/api/user.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -26,6 +26,8 @@ from ..managers.user import UserManager from . import decode_password +from builtins import dict + @app.route('/rest/getUser.view', methods = [ 'GET', 'POST' ]) def user_info(): username = request.values.get('username') @@ -40,7 +42,7 @@ def user_info(): if user is None: return request.error_formatter(70, 'Unknown user') - return request.formatter({ 'user': user.as_subsonic_user() }) + return request.formatter(dict(user = user.as_subsonic_user())) @app.route('/rest/getUsers.view', methods = [ 'GET', 'POST' ]) def users_info(): @@ -48,7 +50,7 @@ def users_info(): return request.error_formatter(50, 'Admin restricted') with db_session: - return request.formatter({ 'users': { 'user': [ u.as_subsonic_user() for u in User.select() ] } }) + return request.formatter(dict(users = dict(user = [ u.as_subsonic_user() for u in User.select() ] ))) @app.route('/rest/createUser.view', methods = [ 'GET', 'POST' ]) def user_add(): @@ -65,7 +67,7 @@ def user_add(): if status == UserManager.NAME_EXISTS: return request.error_formatter(0, 'There is already a user with that username') - return request.formatter({}) + return request.formatter(dict()) @app.route('/rest/deleteUser.view', methods = [ 'GET', 'POST' ]) def user_del(): @@ -85,7 +87,7 @@ def user_del(): if status != UserManager.SUCCESS: return request.error_formatter(0, UserManager.error_str(status)) - return request.formatter({}) + return request.formatter(dict()) @app.route('/rest/changePassword.view', methods = [ 'GET', 'POST' ]) def user_changepass(): @@ -104,5 +106,5 @@ def user_changepass(): code = 70 return request.error_formatter(code, UserManager.error_str(status)) - return request.formatter({}) + return request.formatter(dict()) diff --git a/supysonic/cli.py b/supysonic/cli.py index 251b981..aaecfa1 100755 --- a/supysonic/cli.py +++ b/supysonic/cli.py @@ -4,7 +4,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -65,7 +65,7 @@ class SupysonicCLI(cmd.Cmd): def method(obj, line): try: args = getattr(obj, command + '_parser').parse_args(line.split()) - except RuntimeError, e: + except RuntimeError as e: self.write_error_line(str(e)) return @@ -104,7 +104,7 @@ class SupysonicCLI(cmd.Cmd): if hasattr(self.__class__, 'do_' + command) and not hasattr(self.__class__, 'help_' + command): setattr(self.__class__, 'help_' + command, getattr(self.__class__, parser_name).print_help) if hasattr(self.__class__, command + '_subparsers'): - for action, subparser in getattr(self.__class__, command + '_subparsers').choices.iteritems(): + for action, subparser in getattr(self.__class__, command + '_subparsers').choices.items(): setattr(self, 'help_{} {}'.format(command, action), subparser.print_help) def write_line(self, line = ''): @@ -133,7 +133,7 @@ class SupysonicCLI(cmd.Cmd): num_words = len(line[len(command):begidx].split()) if num_words == 0: - return [ a for a in parsers.choices.keys() if a.startswith(text) ] + return [ a for a in parsers.choices if a.startswith(text) ] return [] folder_parser = CLIParser(prog = 'folder', add_help = False) diff --git a/supysonic/config.py b/supysonic/config.py index 26d0162..c70f8f1 100644 --- a/supysonic/config.py +++ b/supysonic/config.py @@ -4,12 +4,15 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # 2017 Óscar García Amor # # Distributed under terms of the GNU AGPLv3 license. -from ConfigParser import SafeConfigParser +try: + from configparser import ConfigParser +except ImportError: + from ConfigParser import SafeConfigParser as ConfigParser import os import tempfile @@ -52,7 +55,7 @@ class IniConfig(DefaultConfig): ] def __init__(self, paths): - parser = SafeConfigParser() + parser = ConfigParser() parser.read(paths) for section in parser.sections(): diff --git a/supysonic/db.py b/supysonic/db.py index 726db6f..0ce5915 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -26,9 +26,14 @@ from datetime import datetime from pony.orm import Database, Required, Optional, Set, PrimaryKey, LongStr from pony.orm import ObjectNotFound from pony.orm import min, max, avg, sum -from urlparse import urlparse from uuid import UUID, uuid4 +from builtins import dict +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + def now(): return datetime.now().replace(microsecond = 0) @@ -55,13 +60,13 @@ class Folder(db.Entity): ratings = Set(lambda: RatingFolder) def as_subsonic_child(self, user): - info = { - 'id': str(self.id), - 'isDir': True, - 'title': self.name, - 'album': self.name, - 'created': self.created.isoformat() - } + info = dict( + id = str(self.id), + isDir = True, + title = self.name, + album = self.name, + created = self.created.isoformat() + ) if not self.root: info['parent'] = str(self.parent.id) info['artist'] = self.parent.name @@ -95,12 +100,12 @@ class Artist(db.Entity): stars = Set(lambda: StarredArtist) def as_subsonic_artist(self, user): - info = { - 'id': str(self.id), - 'name': self.name, + info = dict( + id = str(self.id), + name = self.name, # coverArt - 'albumCount': self.albums.count() - } + albumCount = self.albums.count() + ) try: starred = StarredArtist[user.id, self.id] @@ -120,15 +125,15 @@ class Album(db.Entity): stars = Set(lambda: StarredAlbum) def as_subsonic_album(self, user): - info = { - 'id': str(self.id), - 'name': self.name, - 'artist': self.artist.name, - 'artistId': str(self.artist.id), - 'songCount': self.tracks.count(), - 'duration': sum(self.tracks.duration), - 'created': min(self.tracks.created).isoformat() - } + info = dict( + id = str(self.id), + name = self.name, + artist = self.artist.name, + artistId = str(self.artist.id), + songCount = self.tracks.count(), + duration = sum(self.tracks.duration), + created = min(self.tracks.created).isoformat() + ) track_with_cover = self.tracks.select(lambda t: t.folder.has_cover_art).first() if track_with_cover is not None: @@ -178,27 +183,27 @@ class Track(db.Entity): ratings = Set(lambda: RatingTrack) def as_subsonic_child(self, user, client): - info = { - 'id': str(self.id), - 'parent': str(self.folder.id), - 'isDir': False, - 'title': self.title, - 'album': self.album.name, - 'artist': self.artist.name, - 'track': self.number, - 'size': os.path.getsize(self.path) if os.path.isfile(self.path) else -1, - 'contentType': self.content_type, - 'suffix': self.suffix(), - 'duration': self.duration, - 'bitRate': self.bitrate, - 'path': self.path[len(self.root_folder.path) + 1:], - 'isVideo': False, - 'discNumber': self.disc, - 'created': self.created.isoformat(), - 'albumId': str(self.album.id), - 'artistId': str(self.artist.id), - 'type': 'music' - } + info = dict( + id = str(self.id), + parent = str(self.folder.id), + isDir = False, + title = self.title, + album = self.album.name, + artist = self.artist.name, + track = self.number, + size = os.path.getsize(self.path) if os.path.isfile(self.path) else -1, + contentType = self.content_type, + suffix = self.suffix(), + duration = self.duration, + bitRate = self.bitrate, + path = self.path[len(self.root_folder.path) + 1:], + isVideo = False, + discNumber = self.disc, + created = self.created.isoformat(), + albumId = str(self.album.id), + artistId = str(self.artist.id), + type = 'music' + ) if self.year: info['year'] = self.year @@ -267,22 +272,22 @@ class User(db.Entity): track_ratings = Set(lambda: RatingTrack, lazy = True) def as_subsonic_user(self): - return { - 'username': self.name, - 'email': self.mail, - 'scrobblingEnabled': self.lastfm_session is not None and self.lastfm_status, - 'adminRole': self.admin, - 'settingsRole': True, - 'downloadRole': True, - 'uploadRole': False, - 'playlistRole': True, - 'coverArtRole': False, - 'commentRole': False, - 'podcastRole': False, - 'streamRole': True, - 'jukeboxRole': False, - 'shareRole': False - } + return dict( + username = self.name, + email = self.mail, + scrobblingEnabled = self.lastfm_session is not None and self.lastfm_status, + adminRole = self.admin, + settingsRole = True, + downloadRole = True, + uploadRole = False, + playlistRole = True, + coverArtRole = False, + commentRole = False, + podcastRole = False, + streamRole = True, + jukeboxRole = False, + shareRole = False + ) class ClientPrefs(db.Entity): _table_ = 'client_prefs' @@ -354,11 +359,11 @@ class ChatMessage(db.Entity): message = Required(str, 512) def responsize(self): - return { - 'username': self.user.name, - 'time': self.time * 1000, - 'message': self.message - } + return dict( + username = self.user.name, + time = self.time * 1000, + message = self.message + ) class Playlist(db.Entity): _table_ = 'playlist' @@ -373,15 +378,15 @@ class Playlist(db.Entity): def as_subsonic_playlist(self, user): tracks = self.get_tracks() - info = { - 'id': str(self.id), - 'name': self.name if self.user.id == user.id else '[%s] %s' % (self.user.name, self.name), - 'owner': self.user.name, - 'public': self.public, - 'songCount': len(tracks), - 'duration': sum(map(lambda t: t.duration, tracks)), - 'created': self.created.isoformat() - } + info = dict( + id = str(self.id), + name = self.name if self.user.id == user.id else '[%s] %s' % (self.user.name, self.name), + owner = self.user.name, + public = self.public, + songCount = len(tracks), + duration = sum(map(lambda t: t.duration, tracks)), + created = self.created.isoformat() + ) if self.comment: info['comment'] = self.comment return info diff --git a/supysonic/frontend/user.py b/supysonic/frontend/user.py index b016090..9a9a422 100644 --- a/supysonic/frontend/user.py +++ b/supysonic/frontend/user.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -18,6 +18,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from builtins import dict + from flask import request, session, flash, render_template, redirect, url_for, current_app as app from functools import wraps from pony.orm import db_session @@ -70,7 +72,7 @@ def user_profile(uid, user): @app.route('/user/', methods = [ 'POST' ]) @me_or_uuid def update_clients(uid, user): - clients_opts = {} + clients_opts = dict() for key, value in request.form.iteritems(): if '_' not in key: continue @@ -82,12 +84,12 @@ def update_clients(uid, user): continue if client not in clients_opts: - clients_opts[client] = { opt: value } + clients_opts[client] = dict(opt = value) else: clients_opts[client][opt] = value app.logger.debug(clients_opts) - for client, opts in clients_opts.iteritems(): + for client, opts in clients_opts.items(): prefs = user.clients.select(lambda c: c.client_name == client).first() if prefs is None: continue diff --git a/supysonic/lastfm.py b/supysonic/lastfm.py index d382854..e09ae1b 100644 --- a/supysonic/lastfm.py +++ b/supysonic/lastfm.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -72,7 +72,7 @@ class LastFm: kwargs['api_key'] = self.__api_key sig_str = '' - for k, v in sorted(kwargs.iteritems()): + for k, v in sorted(kwargs.items()): if type(v) is unicode: sig_str += k + v.encode('utf-8') else: @@ -87,7 +87,7 @@ class LastFm: r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs) else: r = requests.get('http://ws.audioscrobbler.com/2.0/', params = kwargs) - except requests.exceptions.RequestException, e: + except requests.exceptions.RequestException as e: self.__logger.warn('Error while connecting to LastFM: ' + str(e)) return None diff --git a/supysonic/watcher.py b/supysonic/watcher.py index 4a0d90c..9620731 100644 --- a/supysonic/watcher.py +++ b/supysonic/watcher.py @@ -3,7 +3,7 @@ # This file is part of Supysonic. # # Supysonic is a Python implementation of the Subsonic server API. -# Copyright (C) 2014-2017 Alban 'spl0k' Féron +# Copyright (C) 2014-2018 Alban 'spl0k' Féron # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -21,6 +21,8 @@ import logging import time +from builtins import dict + from logging.handlers import TimedRotatingFileHandler from pony.orm import db_session from signal import signal, SIGTERM, SIGINT @@ -47,7 +49,7 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler): def dispatch(self, event): try: super(SupysonicWatcherEventHandler, self).dispatch(event) - except Exception, e: + except Exception as e: self.__logger.critical(e) def on_created(self, event): @@ -117,13 +119,13 @@ class ScannerProcessingQueue(Thread): self.__timeout = delay self.__cond = Condition() self.__timer = None - self.__queue = {} + self.__queue = dict() self.__running = True def run(self): try: self.__run() - except Exception, e: + except Exception as e: self.__logger.critical(e) raise e @@ -194,7 +196,7 @@ class ScannerProcessingQueue(Thread): if not self.__queue: return None - next = min(self.__queue.iteritems(), key = lambda i: i[1].time) + next = min(self.__queue.items(), key = lambda i: i[1].time) if not self.__running or next[1].time + self.__timeout <= time.time(): del self.__queue[next[0]] return next[1] diff --git a/supysonic/web.py b/supysonic/web.py index 3d53b83..bef1380 100644 --- a/supysonic/web.py +++ b/supysonic/web.py @@ -4,7 +4,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2013-2017 Alban 'spl0k' Féron +# Copyright (C) 2013-2018 Alban 'spl0k' Féron # 2017 Óscar García Amor # # Distributed under terms of the GNU AGPLv3 license. @@ -50,7 +50,7 @@ def create_application(config = None): init_database(app.config['BASE']['database_uri']) # Insert unknown mimetypes - for k, v in app.config['MIMETYPES'].iteritems(): + for k, v in app.config['MIMETYPES'].items(): extension = '.' + k.lower() if extension not in mimetypes.types_map: mimetypes.add_type(v, extension, False) diff --git a/tests/api/test_response_helper.py b/tests/api/test_response_helper.py index d445b44..f20c51f 100644 --- a/tests/api/test_response_helper.py +++ b/tests/api/test_response_helper.py @@ -5,7 +5,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2017 Alban 'spl0k' Féron +# Copyright (C) 2017-2018 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. @@ -132,7 +132,7 @@ class ResponseHelperXMLTestCase(ResponseHelperBaseCase): return root def assertAttributesMatchDict(self, elem, d): - d = { k: str(v) for k, v in d.iteritems() } + d = { k: str(v) for k, v in d.items() } self.assertDictEqual(elem.attrib, d) def test_root(self): diff --git a/tests/base/test_cli.py b/tests/base/test_cli.py index 62418c8..e98ce75 100644 --- a/tests/base/test_cli.py +++ b/tests/base/test_cli.py @@ -4,7 +4,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2017 Alban 'spl0k' Féron +# Copyright (C) 2017-2018 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. @@ -16,7 +16,11 @@ import unittest from contextlib import contextmanager from pony.orm import db_session -from StringIO import StringIO + +try: # Don't use io.StringIO on py2, it only accepts unicode and the CLI spits strs + from StringIO import StringIO +except ImportError: + from io import StringIO from supysonic.db import Folder, User, init_database, release_database from supysonic.cli import SupysonicCLI diff --git a/tests/testbase.py b/tests/testbase.py index c604fab..cfa2803 100644 --- a/tests/testbase.py +++ b/tests/testbase.py @@ -4,7 +4,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2017 Alban 'spl0k' Féron +# Copyright (C) 2017-2018 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. @@ -39,7 +39,7 @@ class TestConfig(DefaultConfig): super(TestConfig, self).__init__() for cls in reversed(inspect.getmro(self.__class__)): - for attr, value in cls.__dict__.iteritems(): + for attr, value in cls.__dict__.items(): if attr.startswith('_') or attr != attr.upper(): continue