mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-23 01:16:18 +00:00
parent
4b446f7121
commit
1605fcd202
@ -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)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
flask>=0.9
|
flask>=0.9
|
||||||
|
future
|
||||||
pony
|
pony
|
||||||
Pillow
|
Pillow
|
||||||
simplejson
|
simplejson
|
||||||
|
@ -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):
|
||||||
|
@ -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) ]
|
||||||
}
|
)
|
||||||
})
|
))
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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():
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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 """
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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 ]
|
||||||
}})
|
)))
|
||||||
|
|
||||||
|
@ -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 )))
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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():
|
||||||
|
153
supysonic/db.py
153
supysonic/db.py
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
try: # Don't use io.StringIO on py2, it only accepts unicode and the CLI spits strs
|
||||||
from StringIO import StringIO
|
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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user