1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-23 01:16:18 +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: You'll need these to run Supysonic:
* Python 2.7 * Python 2.7
* [future](http://python-future.org/)
* [Flask](http://flask.pocoo.org/) >= 0.9 * [Flask](http://flask.pocoo.org/) >= 0.9
* [PonyORM](https://ponyorm.com/) * [PonyORM](https://ponyorm.com/)
* [Python Imaging Library](https://github.com/python-pillow/Pillow) * [Python Imaging Library](https://github.com/python-pillow/Pillow)

View File

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

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 ..managers.user import UserManager
from builtins import dict
@app.before_request @app.before_request
def set_formatter(): def set_formatter():
if not request.path.startswith('/rest/'): if not request.path.startswith('/rest/'):
@ -39,19 +41,19 @@ def set_formatter():
if f == 'jsonp': if f == 'jsonp':
# Some clients (MiniSub, Perisonic) set f to jsonp without callback for streamed data # 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' ]: if not callback and request.endpoint not in [ 'stream_media', 'cover_art' ]:
return ResponseHelper.responsize_json({ return ResponseHelper.responsize_json(dict(
'error': { error = dict(
'code': 10, code = 10,
'message': 'Missing callback' message = 'Missing callback'
} )
}, error = True), 400 ), error = True), 400
request.formatter = lambda x, **kwargs: ResponseHelper.responsize_jsonp(x, callback, kwargs) request.formatter = lambda x, **kwargs: ResponseHelper.responsize_jsonp(x, callback, kwargs)
elif f == "json": elif f == "json":
request.formatter = ResponseHelper.responsize_json request.formatter = ResponseHelper.responsize_json
else: else:
request.formatter = ResponseHelper.responsize_xml 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): def decode_password(password):
if not password.startswith('enc:'): if not password.startswith('enc:'):
@ -134,24 +136,29 @@ def not_found(error):
class ResponseHelper: class ResponseHelper:
@staticmethod @staticmethod
def responsize_json(ret, error = False, version = "1.8.0"): def remove_empty_lists(d):
def check_lists(d): if not isinstance(d, dict):
raise TypeError('Expecting a dict')
for key, value in d.items(): for key, value in d.items():
if isinstance(value, dict): if isinstance(value, dict):
d[key] = check_lists(value) d[key] = ResponseHelper.remove_empty_lists(value)
elif isinstance(value, list): elif isinstance(value, list):
if len(value) == 0: if len(value) == 0:
del d[key] del d[key]
else: else:
d[key] = [ check_lists(item) if isinstance(item, dict) else item for item in value ] d[key] = [ ResponseHelper.remove_empty_lists(item) if isinstance(item, dict) else item for item in value ]
return d return d
ret = check_lists(ret) @staticmethod
def responsize_json(ret, error = False, version = "1.8.0"):
ret = ResponseHelper.remove_empty_lists(ret)
# add headers to response # add headers to response
ret.update({ ret.update(
'status': 'failed' if error else 'ok', status = 'failed' if error else 'ok',
'version': version version = version
}) )
return simplejson.dumps({ 'subsonic-response': ret }, indent = True, encoding = 'utf-8') return simplejson.dumps({ 'subsonic-response': ret }, indent = True, encoding = 'utf-8')
@staticmethod @staticmethod
@ -161,11 +168,11 @@ class ResponseHelper:
@staticmethod @staticmethod
def responsize_xml(ret, error = False, version = "1.8.0"): def responsize_xml(ret, error = False, version = "1.8.0"):
"""Return an xml response from json and replace unsupported characters.""" """Return an xml response from json and replace unsupported characters."""
ret.update({ ret.update(
'status': 'failed' if error else 'ok', status = 'failed' if error else 'ok',
'version': version, version = version,
'xmlns': "http://subsonic.org/restapi" xmlns = "http://subsonic.org/restapi"
}) )
elem = ElementTree.Element('subsonic-response') elem = ElementTree.Element('subsonic-response')
ResponseHelper.dict2xml(elem, ret) ResponseHelper.dict2xml(elem, ret)
@ -184,27 +191,24 @@ class ResponseHelper:
""" """
if not isinstance(dictionary, dict): if not isinstance(dictionary, dict):
raise TypeError('Expecting a 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') raise TypeError('Dictionary keys must be strings')
subelems = { k: v for k, v in dictionary.iteritems() if isinstance(v, dict) } for name, value in dictionary.items():
sequences = { k: v for k, v in dictionary.iteritems() if isinstance(v, list) } if name == '_value_':
attributes = { k: v for k, v in dictionary.iteritems() if k != '_value_' and k not in subelems and k not in sequences } elem.text = ResponseHelper.value_tostring(value)
elif isinstance(value, dict):
if '_value_' in dictionary: subelem = ElementTree.SubElement(elem, name)
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) 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: else:
subelem.text = ResponseHelper.value_tostring(value) subelem.text = ResponseHelper.value_tostring(v)
else:
elem.set(name, ResponseHelper.value_tostring(value))
@staticmethod @staticmethod
def value_tostring(value): def value_tostring(value):

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User
from ..db import now from ..db import now
from builtins import dict
@app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ])
def rand_songs(): def rand_songs():
size = request.values.get('size', '10') size = request.values.get('size', '10')
@ -56,11 +58,11 @@ def rand_songs():
query = query.filter(lambda t: t.root_folder.id == fid) query = query.filter(lambda t: t.root_folder.id == fid)
with db_session: with db_session:
return request.formatter({ return request.formatter(dict(
'randomSongs': { randomSongs = dict(
'song': [ t.as_subsonic_child(request.user, request.client) for t in query.random(size) ] song = [ t.as_subsonic_child(request.user, request.client) for t in query.random(size) ]
} )
}) ))
@app.route('/rest/getAlbumList.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getAlbumList.view', methods = [ 'GET', 'POST' ])
def album_list(): def album_list():
@ -76,11 +78,11 @@ def album_list():
query = select(t.folder for t in Track) query = select(t.folder for t in Track)
if ltype == 'random': if ltype == 'random':
with db_session: with db_session:
return request.formatter({ return request.formatter(dict(
'albumList': { albumList = dict(
'album': [ a.as_subsonic_child(request.user) for a in query.random(size) ] album = [ a.as_subsonic_child(request.user) for a in query.random(size) ]
} )
}) ))
elif ltype == 'newest': elif ltype == 'newest':
query = query.order_by(desc(Folder.created)) query = query.order_by(desc(Folder.created))
elif ltype == 'highest': elif ltype == 'highest':
@ -99,11 +101,11 @@ def album_list():
return request.error_formatter(0, 'Unknown search type') return request.error_formatter(0, 'Unknown search type')
with db_session: with db_session:
return request.formatter({ return request.formatter(dict(
'albumList': { albumList = dict(
'album': [ f.as_subsonic_child(request.user) for f in query.limit(size, offset) ] album = [ f.as_subsonic_child(request.user) for f in query.limit(size, offset) ]
} )
}) ))
@app.route('/rest/getAlbumList2.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getAlbumList2.view', methods = [ 'GET', 'POST' ])
def album_list_id3(): def album_list_id3():
@ -119,11 +121,11 @@ def album_list_id3():
query = Album.select() query = Album.select()
if ltype == 'random': if ltype == 'random':
with db_session: with db_session:
return request.formatter({ return request.formatter(dict(
'albumList2': { albumList2 = dict(
'album': [ a.as_subsonic_album(request.user) for a in query.random(size) ] album = [ a.as_subsonic_album(request.user) for a in query.random(size) ]
} )
}) ))
elif ltype == 'newest': elif ltype == 'newest':
query = query.order_by(lambda a: desc(min(a.tracks.created))) query = query.order_by(lambda a: desc(min(a.tracks.created)))
elif ltype == 'frequent': elif ltype == 'frequent':
@ -140,47 +142,47 @@ def album_list_id3():
return request.error_formatter(0, 'Unknown search type') return request.error_formatter(0, 'Unknown search type')
with db_session: with db_session:
return request.formatter({ return request.formatter(dict(
'albumList2': { albumList2 = dict(
'album': [ f.as_subsonic_album(request.user) for f in query.limit(size, offset) ] album = [ f.as_subsonic_album(request.user) for f in query.limit(size, offset) ]
} )
}) ))
@app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
def now_playing(): def now_playing():
query = User.select(lambda u: u.last_play is not None and u.last_play_date + timedelta(minutes = 3) > now()) query = User.select(lambda u: u.last_play is not None and u.last_play_date + timedelta(minutes = 3) > now())
return request.formatter({ return request.formatter(dict(
'nowPlaying': { nowPlaying = dict(
'entry': [ dict( entry = [ dict(
u.last_play.as_subsonic_child(request.user, request.client).items() + u.last_play.as_subsonic_child(request.user, request.client),
{ 'username': u.name, 'minutesAgo': (now() - u.last_play_date).seconds / 60, 'playerId': 0 }.items() username = u.name, minutesAgo = (now() - u.last_play_date).seconds / 60, playerId = 0
) for u in query ] ) for u in query ]
} )
}) ))
@app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
def get_starred(): def get_starred():
folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id) folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id)
return request.formatter({ return request.formatter(dict(
'starred': { starred = dict(
'artist': [ { 'id': str(sf.id), 'name': sf.name } for sf in folders.filter(lambda f: count(f.tracks) == 0) ], 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) ], 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) ] 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' ]) @app.route('/rest/getStarred2.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
def get_starred_id3(): def get_starred_id3():
return request.formatter({ return request.formatter(dict(
'starred2': { 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) ], 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) ], 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) ] 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. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 . import get_entity
from builtins import dict
@db_session @db_session
def try_star(cls, starred_cls, eid): def try_star(cls, starred_cls, eid):
""" Stars an entity """ Stars an entity
@ -45,16 +47,16 @@ def try_star(cls, starred_cls, eid):
try: try:
uid = uuid.UUID(eid) uid = uuid.UUID(eid)
except: except:
return { 'code': 0, 'message': 'Invalid {} id {}'.format(cls.__name__, eid) } return dict(code = 0, message = 'Invalid {} id {}'.format(cls.__name__, eid))
try: try:
e = cls[uid] e = cls[uid]
except ObjectNotFound: except ObjectNotFound:
return { 'code': 70, 'message': 'Unknown {} id {}'.format(cls.__name__, eid) } return dict(code = 70, message = 'Unknown {} id {}'.format(cls.__name__, eid))
try: try:
starred_cls[request.user.id, uid] 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: except ObjectNotFound:
pass pass
@ -73,7 +75,7 @@ def try_unstar(starred_cls, eid):
try: try:
uid = uuid.UUID(eid) uid = uuid.UUID(eid)
except: 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) delete(s for s in starred_cls if s.user.id == request.user.id and s.starred.id == uid)
return None return None
@ -85,7 +87,7 @@ def merge_errors(errors):
error = errors[0] error = errors[0]
elif len(errors) > 1: elif len(errors) > 1:
codes = set(map(lambda e: e['code'], errors)) 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 return error
@ -110,7 +112,7 @@ def star():
errors.append(try_star(Artist, StarredArtist, arId)) errors.append(try_star(Artist, StarredArtist, arId))
error = merge_errors(errors) 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' ]) @app.route('/rest/unstar.view', methods = [ 'GET', 'POST' ])
def unstar(): def unstar():
@ -133,7 +135,7 @@ def unstar():
errors.append(try_unstar(StarredArtist, arId)) errors.append(try_unstar(StarredArtist, arId))
error = merge_errors(errors) 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' ]) @app.route('/rest/setRating.view', methods = [ 'GET', 'POST' ])
def rate(): def rate():
@ -171,7 +173,7 @@ def rate():
except ObjectNotFound: except ObjectNotFound:
rating_cls(user = User[request.user.id], rated = rated, rating = rating) 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' ]) @app.route('/rest/scrobble.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
@ -197,5 +199,5 @@ def scrobble():
else: else:
lfm.now_playing(res) lfm.now_playing(res)
return request.formatter({}) return request.formatter(dict())

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 . import get_entity
from builtins import dict
@app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
def list_folders(): def list_folders():
return request.formatter({ return request.formatter(dict(
'musicFolders': { musicFolders = dict(
'musicFolder': [ { musicFolder = [ dict(
'id': str(f.id), id = str(f.id),
'name': f.name name = f.name
} for f in Folder.select(lambda f: f.root).order_by(Folder.name) ] ) for f in Folder.select(lambda f: f.root).order_by(Folder.name) ]
} )
}) ))
@app.route('/rest/getIndexes.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getIndexes.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
@ -70,7 +72,7 @@ def list_indexes():
last_modif = max(map(lambda f: f.last_scan, folders)) last_modif = max(map(lambda f: f.last_scan, folders))
if ifModifiedSince is not None and last_modif < ifModifiedSince: 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 # The XSD lies, we don't return artists but a directory structure
artists = [] artists = []
@ -79,7 +81,7 @@ def list_indexes():
artists += f.children.select()[:] artists += f.children.select()[:]
children += f.tracks.select()[:] children += f.tracks.select()[:]
indexes = {} indexes = dict()
for artist in artists: for artist in artists:
index = artist.name[0].upper() index = artist.name[0].upper()
if index in map(str, xrange(10)): if index in map(str, xrange(10)):
@ -92,19 +94,19 @@ def list_indexes():
indexes[index].append(artist) indexes[index].append(artist)
return request.formatter({ return request.formatter(dict(
'indexes': { indexes = dict(
'lastModified': last_modif * 1000, lastModified = last_modif * 1000,
'index': [ { index = [ dict(
'name': k, name = k,
'artist': [ { artist = [ dict(
'id': str(a.id), id = str(a.id),
'name': a.name name = a.name
} for a in sorted(v, key = lambda a: a.name.lower()) ] ) for a in sorted(v, key = lambda a: a.name.lower()) ]
} for k, v in sorted(indexes.iteritems()) ], ) 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()) ] 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' ]) @app.route('/rest/getMusicDirectory.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
@ -113,21 +115,21 @@ def show_directory():
if not status: if not status:
return res return res
directory = { directory = dict(
'id': str(res.id), id = str(res.id),
'name': res.name, 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()) ] 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: if not res.root:
directory['parent'] = str(res.parent.id) 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' ]) @app.route('/rest/getArtists.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
def list_artists(): def list_artists():
# According to the API page, there are no parameters? # According to the API page, there are no parameters?
indexes = {} indexes = dict()
for artist in Artist.select(): for artist in Artist.select():
index = artist.name[0].upper() if artist.name else '?' index = artist.name[0].upper() if artist.name else '?'
if index in map(str, xrange(10)): if index in map(str, xrange(10)):
@ -140,14 +142,14 @@ def list_artists():
indexes[index].append(artist) indexes[index].append(artist)
return request.formatter({ return request.formatter(dict(
'artists': { artists = dict(
'index': [ { index = [ dict(
'name': k, name = k,
'artist': [ a.as_subsonic_artist(request.user) for a in sorted(v, key = lambda a: a.name.lower()) ] 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()) ] ) for k, v in sorted(indexes.items()) ]
} )
}) ))
@app.route('/rest/getArtist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getArtist.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
@ -161,7 +163,7 @@ def artist_info():
albums |= { t.album for t in res.tracks } 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()) ] 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' ]) @app.route('/rest/getAlbum.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
@ -173,7 +175,7 @@ def album_info():
info = res.as_subsonic_album(request.user) 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()) ] 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' ]) @app.route('/rest/getSong.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
@ -182,7 +184,7 @@ def track_info():
if not status: if not status:
return res 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' ]) @app.route('/rest/getVideos.view', methods = [ 'GET', 'POST' ])
def list_videos(): def list_videos():

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 ..db import ChatMessage, User
from builtins import dict
@app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ])
def get_chat(): def get_chat():
since = request.values.get('since') since = request.values.get('since')
@ -36,7 +38,7 @@ def get_chat():
if since: if since:
query = query.filter(lambda m: m.time > 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' ]) @app.route('/rest/addChatMessage.view', methods = [ 'GET', 'POST' ])
def add_chat_message(): def add_chat_message():
@ -47,5 +49,5 @@ def add_chat_message():
with db_session: with db_session:
ChatMessage(user = User[request.user.id], message = msg) 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. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 . import get_entity
from builtins import dict
def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate): def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate):
if not base_cmdline: if not base_cmdline:
return None return None
@ -195,11 +197,11 @@ def lyrics():
app.logger.warn('Unsupported encoding for lyrics file ' + lyrics_path) app.logger.warn('Unsupported encoding for lyrics file ' + lyrics_path)
continue continue
return request.formatter({ 'lyrics': { return request.formatter(dict(lyrics = dict(
'artist': track.album.artist.name, artist = track.album.artist.name,
'title': track.title, title = track.title,
'_value_': lyrics _value_ = lyrics
} }) )))
try: try:
r = requests.get("http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect", r = requests.get("http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect",
@ -207,15 +209,15 @@ def lyrics():
root = ElementTree.fromstring(r.content) root = ElementTree.fromstring(r.content)
ns = { 'cl': 'http://api.chartlyrics.com/' } ns = { 'cl': 'http://api.chartlyrics.com/' }
return request.formatter({ 'lyrics': { return request.formatter(dict(lyrics = dict(
'artist': root.find('cl:LyricArtist', namespaces = ns).text, artist = root.find('cl:LyricArtist', namespaces = ns).text,
'title': root.find('cl:LyricSong', namespaces = ns).text, title = root.find('cl:LyricSong', namespaces = ns).text,
'_value_': root.find('cl:Lyric', namespaces = ns).text _value_ = root.find('cl:Lyric', namespaces = ns).text
} }) )))
except requests.exceptions.RequestException, e: except requests.exceptions.RequestException as e:
app.logger.warn('Error while requesting the ChartLyrics API: ' + str(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): def read_file_as_unicode(path):
""" Opens a file trying with different encodings and returns the contents as a unicode string """ """ 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. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 . import get_entity
from builtins import dict
@app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ])
def list_playlists(): def list_playlists():
query = Playlist.select(lambda p: p.user.id == request.user.id or p.public).order_by(Playlist.name) 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) query = Playlist.select(lambda p: p.user.name == username).order_by(Playlist.name)
with db_session: 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' ]) @app.route('/rest/getPlaylist.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
@ -59,7 +61,7 @@ def show_playlist():
info = res.as_subsonic_playlist(request.user) info = res.as_subsonic_playlist(request.user)
info['entry'] = [ t.as_subsonic_child(request.user, request.client) for t in res.get_tracks() ] 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' ]) @app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
@ -99,7 +101,7 @@ def create_playlist():
playlist.add(track) playlist.add(track)
return request.formatter({}) return request.formatter(dict())
@app.route('/rest/deletePlaylist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/deletePlaylist.view', methods = [ 'GET', 'POST' ])
@db_session @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") return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours")
res.delete() res.delete()
return request.formatter({}) return request.formatter(dict())
@app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ])
@db_session @db_session
@ -149,5 +151,5 @@ def update_playlist():
playlist.remove_at_indexes(to_remove) 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. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import OrderedDict
from datetime import datetime from datetime import datetime
from flask import request, current_app as app from flask import request, current_app as app
from pony.orm import db_session, select from pony.orm import db_session, select
from ..db import Folder, Track, Artist, Album from ..db import Folder, Track, Artist, Album
from builtins import dict
@app.route('/rest/search.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/search.view', methods = [ 'GET', 'POST' ])
def old_search(): def old_search():
artist, album, title, anyf, count, offset, newer_than = map(request.values.get, [ 'artist', 'album', 'title', 'any', 'count', 'offset', 'newerThan' ]) 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 tend = offset + count - fcount
res += tracks[toff : tend] res += tracks[toff : tend]
return request.formatter({ 'searchResult': { return request.formatter(dict(searchResult = dict(
'totalHits': folders.count() + tracks.count(), totalHits = folders.count() + tracks.count(),
'offset': offset, 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 ] 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: else:
return request.error_formatter(10, 'Missing search parameter') return request.error_formatter(10, 'Missing search parameter')
with db_session: with db_session:
return request.formatter({ 'searchResult': { return request.formatter(dict(searchResult = dict(
'totalHits': query.count(), totalHits = query.count(),
'offset': offset, 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] ] 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' ]) @app.route('/rest/search2.view', methods = [ 'GET', 'POST' ])
def new_search(): 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) 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) songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset)
return request.formatter({ 'searchResult2': { return request.formatter(dict(searchResult2 = OrderedDict(
'artist': [ { 'id': str(a.id), 'name': a.name } for a in artists ], artist = [ dict(id = str(a.id), name = a.name) for a in artists ],
'album': [ f.as_subsonic_child(request.user) for f in albums ], album = [ f.as_subsonic_child(request.user) for f in albums ],
'song': [ t.as_subsonic_child(request.user, request.client) for t in songs ] song = [ t.as_subsonic_child(request.user, request.client) for t in songs ]
}}) )))
@app.route('/rest/search3.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/search3.view', methods = [ 'GET', 'POST' ])
def search_id3(): def search_id3():
@ -120,9 +123,9 @@ def search_id3():
albums = Album.select(lambda a: query in a.name).limit(album_count, album_offset) 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) songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset)
return request.formatter({ 'searchResult3': { return request.formatter(dict(searchResult3 = OrderedDict(
'artist': [ a.as_subsonic_artist(request.user) for a in artists ], artist = [ a.as_subsonic_artist(request.user) for a in artists ],
'album': [ a.as_subsonic_album(request.user) for a in albums ], album = [ a.as_subsonic_album(request.user) for a in albums ],
'song': [ t.as_subsonic_child(request.user, request.client) for t in songs ] 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. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 flask import request, current_app as app
from builtins import dict
@app.route('/rest/ping.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/ping.view', methods = [ 'GET', 'POST' ])
def ping(): def ping():
return request.formatter({}) return request.formatter(dict())
@app.route('/rest/getLicense.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getLicense.view', methods = [ 'GET', 'POST' ])
def license(): 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. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 . import decode_password
from builtins import dict
@app.route('/rest/getUser.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getUser.view', methods = [ 'GET', 'POST' ])
def user_info(): def user_info():
username = request.values.get('username') username = request.values.get('username')
@ -40,7 +42,7 @@ def user_info():
if user is None: if user is None:
return request.error_formatter(70, 'Unknown user') 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' ]) @app.route('/rest/getUsers.view', methods = [ 'GET', 'POST' ])
def users_info(): def users_info():
@ -48,7 +50,7 @@ def users_info():
return request.error_formatter(50, 'Admin restricted') return request.error_formatter(50, 'Admin restricted')
with db_session: 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' ]) @app.route('/rest/createUser.view', methods = [ 'GET', 'POST' ])
def user_add(): def user_add():
@ -65,7 +67,7 @@ def user_add():
if status == UserManager.NAME_EXISTS: if status == UserManager.NAME_EXISTS:
return request.error_formatter(0, 'There is already a user with that username') 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' ]) @app.route('/rest/deleteUser.view', methods = [ 'GET', 'POST' ])
def user_del(): def user_del():
@ -85,7 +87,7 @@ def user_del():
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
return request.error_formatter(0, UserManager.error_str(status)) return request.error_formatter(0, UserManager.error_str(status))
return request.formatter({}) return request.formatter(dict())
@app.route('/rest/changePassword.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/changePassword.view', methods = [ 'GET', 'POST' ])
def user_changepass(): def user_changepass():
@ -104,5 +106,5 @@ def user_changepass():
code = 70 code = 70
return request.error_formatter(code, UserManager.error_str(status)) 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. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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): def method(obj, line):
try: try:
args = getattr(obj, command + '_parser').parse_args(line.split()) args = getattr(obj, command + '_parser').parse_args(line.split())
except RuntimeError, e: except RuntimeError as e:
self.write_error_line(str(e)) self.write_error_line(str(e))
return return
@ -104,7 +104,7 @@ class SupysonicCLI(cmd.Cmd):
if hasattr(self.__class__, 'do_' + command) and not hasattr(self.__class__, 'help_' + command): 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) setattr(self.__class__, 'help_' + command, getattr(self.__class__, parser_name).print_help)
if hasattr(self.__class__, command + '_subparsers'): 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) setattr(self, 'help_{} {}'.format(command, action), subparser.print_help)
def write_line(self, line = ''): def write_line(self, line = ''):
@ -133,7 +133,7 @@ class SupysonicCLI(cmd.Cmd):
num_words = len(line[len(command):begidx].split()) num_words = len(line[len(command):begidx].split())
if num_words == 0: 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 [] return []
folder_parser = CLIParser(prog = 'folder', add_help = False) folder_parser = CLIParser(prog = 'folder', add_help = False)

View File

@ -4,12 +4,15 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 2017 Óscar García Amor
# #
# Distributed under terms of the GNU AGPLv3 license. # 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 os
import tempfile import tempfile
@ -52,7 +55,7 @@ class IniConfig(DefaultConfig):
] ]
def __init__(self, paths): def __init__(self, paths):
parser = SafeConfigParser() parser = ConfigParser()
parser.read(paths) parser.read(paths)
for section in parser.sections(): for section in parser.sections():

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 Database, Required, Optional, Set, PrimaryKey, LongStr
from pony.orm import ObjectNotFound from pony.orm import ObjectNotFound
from pony.orm import min, max, avg, sum from pony.orm import min, max, avg, sum
from urlparse import urlparse
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from builtins import dict
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
def now(): def now():
return datetime.now().replace(microsecond = 0) return datetime.now().replace(microsecond = 0)
@ -55,13 +60,13 @@ class Folder(db.Entity):
ratings = Set(lambda: RatingFolder) ratings = Set(lambda: RatingFolder)
def as_subsonic_child(self, user): def as_subsonic_child(self, user):
info = { info = dict(
'id': str(self.id), id = str(self.id),
'isDir': True, isDir = True,
'title': self.name, title = self.name,
'album': self.name, album = self.name,
'created': self.created.isoformat() created = self.created.isoformat()
} )
if not self.root: if not self.root:
info['parent'] = str(self.parent.id) info['parent'] = str(self.parent.id)
info['artist'] = self.parent.name info['artist'] = self.parent.name
@ -95,12 +100,12 @@ class Artist(db.Entity):
stars = Set(lambda: StarredArtist) stars = Set(lambda: StarredArtist)
def as_subsonic_artist(self, user): def as_subsonic_artist(self, user):
info = { info = dict(
'id': str(self.id), id = str(self.id),
'name': self.name, name = self.name,
# coverArt # coverArt
'albumCount': self.albums.count() albumCount = self.albums.count()
} )
try: try:
starred = StarredArtist[user.id, self.id] starred = StarredArtist[user.id, self.id]
@ -120,15 +125,15 @@ class Album(db.Entity):
stars = Set(lambda: StarredAlbum) stars = Set(lambda: StarredAlbum)
def as_subsonic_album(self, user): def as_subsonic_album(self, user):
info = { info = dict(
'id': str(self.id), id = str(self.id),
'name': self.name, name = self.name,
'artist': self.artist.name, artist = self.artist.name,
'artistId': str(self.artist.id), artistId = str(self.artist.id),
'songCount': self.tracks.count(), songCount = self.tracks.count(),
'duration': sum(self.tracks.duration), duration = sum(self.tracks.duration),
'created': min(self.tracks.created).isoformat() created = min(self.tracks.created).isoformat()
} )
track_with_cover = self.tracks.select(lambda t: t.folder.has_cover_art).first() track_with_cover = self.tracks.select(lambda t: t.folder.has_cover_art).first()
if track_with_cover is not None: if track_with_cover is not None:
@ -178,27 +183,27 @@ class Track(db.Entity):
ratings = Set(lambda: RatingTrack) ratings = Set(lambda: RatingTrack)
def as_subsonic_child(self, user, client): def as_subsonic_child(self, user, client):
info = { info = dict(
'id': str(self.id), id = str(self.id),
'parent': str(self.folder.id), parent = str(self.folder.id),
'isDir': False, isDir = False,
'title': self.title, title = self.title,
'album': self.album.name, album = self.album.name,
'artist': self.artist.name, artist = self.artist.name,
'track': self.number, track = self.number,
'size': os.path.getsize(self.path) if os.path.isfile(self.path) else -1, size = os.path.getsize(self.path) if os.path.isfile(self.path) else -1,
'contentType': self.content_type, contentType = self.content_type,
'suffix': self.suffix(), suffix = self.suffix(),
'duration': self.duration, duration = self.duration,
'bitRate': self.bitrate, bitRate = self.bitrate,
'path': self.path[len(self.root_folder.path) + 1:], path = self.path[len(self.root_folder.path) + 1:],
'isVideo': False, isVideo = False,
'discNumber': self.disc, discNumber = self.disc,
'created': self.created.isoformat(), created = self.created.isoformat(),
'albumId': str(self.album.id), albumId = str(self.album.id),
'artistId': str(self.artist.id), artistId = str(self.artist.id),
'type': 'music' type = 'music'
} )
if self.year: if self.year:
info['year'] = self.year info['year'] = self.year
@ -267,22 +272,22 @@ class User(db.Entity):
track_ratings = Set(lambda: RatingTrack, lazy = True) track_ratings = Set(lambda: RatingTrack, lazy = True)
def as_subsonic_user(self): def as_subsonic_user(self):
return { return dict(
'username': self.name, username = self.name,
'email': self.mail, email = self.mail,
'scrobblingEnabled': self.lastfm_session is not None and self.lastfm_status, scrobblingEnabled = self.lastfm_session is not None and self.lastfm_status,
'adminRole': self.admin, adminRole = self.admin,
'settingsRole': True, settingsRole = True,
'downloadRole': True, downloadRole = True,
'uploadRole': False, uploadRole = False,
'playlistRole': True, playlistRole = True,
'coverArtRole': False, coverArtRole = False,
'commentRole': False, commentRole = False,
'podcastRole': False, podcastRole = False,
'streamRole': True, streamRole = True,
'jukeboxRole': False, jukeboxRole = False,
'shareRole': False shareRole = False
} )
class ClientPrefs(db.Entity): class ClientPrefs(db.Entity):
_table_ = 'client_prefs' _table_ = 'client_prefs'
@ -354,11 +359,11 @@ class ChatMessage(db.Entity):
message = Required(str, 512) message = Required(str, 512)
def responsize(self): def responsize(self):
return { return dict(
'username': self.user.name, username = self.user.name,
'time': self.time * 1000, time = self.time * 1000,
'message': self.message message = self.message
} )
class Playlist(db.Entity): class Playlist(db.Entity):
_table_ = 'playlist' _table_ = 'playlist'
@ -373,15 +378,15 @@ class Playlist(db.Entity):
def as_subsonic_playlist(self, user): def as_subsonic_playlist(self, user):
tracks = self.get_tracks() tracks = self.get_tracks()
info = { info = dict(
'id': str(self.id), id = str(self.id),
'name': self.name if self.user.id == user.id else '[%s] %s' % (self.user.name, self.name), name = self.name if self.user.id == user.id else '[%s] %s' % (self.user.name, self.name),
'owner': self.user.name, owner = self.user.name,
'public': self.public, public = self.public,
'songCount': len(tracks), songCount = len(tracks),
'duration': sum(map(lambda t: t.duration, tracks)), duration = sum(map(lambda t: t.duration, tracks)),
'created': self.created.isoformat() created = self.created.isoformat()
} )
if self.comment: if self.comment:
info['comment'] = self.comment info['comment'] = self.comment
return info return info

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from builtins import dict
from flask import request, session, flash, render_template, redirect, url_for, current_app as app from flask import request, session, flash, render_template, redirect, url_for, current_app as app
from functools import wraps from functools import wraps
from pony.orm import db_session from pony.orm import db_session
@ -70,7 +72,7 @@ def user_profile(uid, user):
@app.route('/user/<uid>', methods = [ 'POST' ]) @app.route('/user/<uid>', methods = [ 'POST' ])
@me_or_uuid @me_or_uuid
def update_clients(uid, user): def update_clients(uid, user):
clients_opts = {} clients_opts = dict()
for key, value in request.form.iteritems(): for key, value in request.form.iteritems():
if '_' not in key: if '_' not in key:
continue continue
@ -82,12 +84,12 @@ def update_clients(uid, user):
continue continue
if client not in clients_opts: if client not in clients_opts:
clients_opts[client] = { opt: value } clients_opts[client] = dict(opt = value)
else: else:
clients_opts[client][opt] = value clients_opts[client][opt] = value
app.logger.debug(clients_opts) 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() prefs = user.clients.select(lambda c: c.client_name == client).first()
if prefs is None: if prefs is None:
continue continue

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # 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 kwargs['api_key'] = self.__api_key
sig_str = '' sig_str = ''
for k, v in sorted(kwargs.iteritems()): for k, v in sorted(kwargs.items()):
if type(v) is unicode: if type(v) is unicode:
sig_str += k + v.encode('utf-8') sig_str += k + v.encode('utf-8')
else: else:
@ -87,7 +87,7 @@ class LastFm:
r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs) r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs)
else: else:
r = requests.get('http://ws.audioscrobbler.com/2.0/', params = kwargs) 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)) self.__logger.warn('Error while connecting to LastFM: ' + str(e))
return None return None

View File

@ -3,7 +3,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# #
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 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 # it under the terms of the GNU Affero General Public License as published by
@ -21,6 +21,8 @@
import logging import logging
import time import time
from builtins import dict
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from pony.orm import db_session from pony.orm import db_session
from signal import signal, SIGTERM, SIGINT from signal import signal, SIGTERM, SIGINT
@ -47,7 +49,7 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler):
def dispatch(self, event): def dispatch(self, event):
try: try:
super(SupysonicWatcherEventHandler, self).dispatch(event) super(SupysonicWatcherEventHandler, self).dispatch(event)
except Exception, e: except Exception as e:
self.__logger.critical(e) self.__logger.critical(e)
def on_created(self, event): def on_created(self, event):
@ -117,13 +119,13 @@ class ScannerProcessingQueue(Thread):
self.__timeout = delay self.__timeout = delay
self.__cond = Condition() self.__cond = Condition()
self.__timer = None self.__timer = None
self.__queue = {} self.__queue = dict()
self.__running = True self.__running = True
def run(self): def run(self):
try: try:
self.__run() self.__run()
except Exception, e: except Exception as e:
self.__logger.critical(e) self.__logger.critical(e)
raise e raise e
@ -194,7 +196,7 @@ class ScannerProcessingQueue(Thread):
if not self.__queue: if not self.__queue:
return None 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(): if not self.__running or next[1].time + self.__timeout <= time.time():
del self.__queue[next[0]] del self.__queue[next[0]]
return next[1] return next[1]

View File

@ -4,7 +4,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 2017 Óscar García Amor
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
@ -50,7 +50,7 @@ def create_application(config = None):
init_database(app.config['BASE']['database_uri']) init_database(app.config['BASE']['database_uri'])
# Insert unknown mimetypes # Insert unknown mimetypes
for k, v in app.config['MIMETYPES'].iteritems(): for k, v in app.config['MIMETYPES'].items():
extension = '.' + k.lower() extension = '.' + k.lower()
if extension not in mimetypes.types_map: if extension not in mimetypes.types_map:
mimetypes.add_type(v, extension, False) mimetypes.add_type(v, extension, False)

View File

@ -5,7 +5,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
@ -132,7 +132,7 @@ class ResponseHelperXMLTestCase(ResponseHelperBaseCase):
return root return root
def assertAttributesMatchDict(self, elem, d): 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) self.assertDictEqual(elem.attrib, d)
def test_root(self): def test_root(self):

View File

@ -4,7 +4,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
@ -16,7 +16,11 @@ import unittest
from contextlib import contextmanager from contextlib import contextmanager
from pony.orm import db_session 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.db import Folder, User, init_database, release_database
from supysonic.cli import SupysonicCLI from supysonic.cli import SupysonicCLI

View File

@ -4,7 +4,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
@ -39,7 +39,7 @@ class TestConfig(DefaultConfig):
super(TestConfig, self).__init__() super(TestConfig, self).__init__()
for cls in reversed(inspect.getmro(self.__class__)): 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(): if attr.startswith('_') or attr != attr.upper():
continue continue