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