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

Py3: imports, exceptions, dicts

Ref #75
This commit is contained in:
spl0k 2018-01-06 00:33:45 +01:00
parent 4b446f7121
commit 1605fcd202
22 changed files with 341 additions and 300 deletions

View File

@ -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)

View File

@ -1,4 +1,5 @@
flask>=0.9
future
pony
Pillow
simplejson

View File

@ -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):

View File

@ -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) ]
)
))

View File

@ -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())

View File

@ -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():

View File

@ -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())

View File

@ -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 """

View File

@ -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())

View File

@ -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 <http://www.gnu.org/licenses/>.
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 ]
)))

View File

@ -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 )))

View File

@ -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())

View File

@ -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)

View File

@ -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():

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
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/<uid>', 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

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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