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